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 {
...
@@ -200,6 +200,8 @@ type PublicSettings struct {
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
WeChatOAuthEnabled
bool
`json:"wechat_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"`
OIDCOAuthEnabled
bool
`json:"oidc_oauth_enabled"`
OIDCOAuthProviderName
string
`json:"oidc_oauth_provider_name"`
OIDCOAuthProviderName
string
`json:"oidc_oauth_provider_name"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
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) {
...
@@ -58,6 +58,8 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
WeChatOAuthEnabled
:
settings
.
WeChatOAuthEnabled
,
WeChatOAuthEnabled
:
settings
.
WeChatOAuthEnabled
,
WeChatOAuthOpenEnabled
:
settings
.
WeChatOAuthOpenEnabled
,
WeChatOAuthMPEnabled
:
settings
.
WeChatOAuthMPEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
OIDCOAuthProviderName
:
settings
.
OIDCOAuthProviderName
,
OIDCOAuthProviderName
:
settings
.
OIDCOAuthProviderName
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
...
...
backend/internal/handler/setting_handler_public_test.go
View file @
067eb23d
...
@@ -81,3 +81,35 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
...
@@ -81,3 +81,35 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
True
(
t
,
resp
.
Data
.
ForceEmailOnThirdPartySignup
)
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
...
@@ -274,7 +274,9 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
if
oidcProviderName
==
""
{
if
oidcProviderName
==
""
{
oidcProviderName
=
"OIDC"
oidcProviderName
=
"OIDC"
}
}
weChatEnabled
:=
isWeChatOAuthConfigured
()
weChatOpenEnabled
:=
isWeChatOAuthOpenConfigured
()
weChatMPEnabled
:=
isWeChatOAuthMPConfigured
()
weChatEnabled
:=
weChatOpenEnabled
||
weChatMPEnabled
// Password reset requires email verification to be enabled
// Password reset requires email verification to be enabled
emailVerifyEnabled
:=
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
emailVerifyEnabled
:=
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
...
@@ -319,6 +321,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -319,6 +321,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
CustomEndpoints
:
settings
[
SettingKeyCustomEndpoints
],
CustomEndpoints
:
settings
[
SettingKeyCustomEndpoints
],
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
WeChatOAuthEnabled
:
weChatEnabled
,
WeChatOAuthEnabled
:
weChatEnabled
,
WeChatOAuthOpenEnabled
:
weChatOpenEnabled
,
WeChatOAuthMPEnabled
:
weChatMPEnabled
,
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
PaymentEnabled
:
settings
[
SettingPaymentEnabled
]
==
"true"
,
PaymentEnabled
:
settings
[
SettingPaymentEnabled
]
==
"true"
,
OIDCOAuthEnabled
:
oidcEnabled
,
OIDCOAuthEnabled
:
oidcEnabled
,
...
@@ -376,6 +380,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
...
@@ -376,6 +380,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
CustomEndpoints
json
.
RawMessage
`json:"custom_endpoints"`
CustomEndpoints
json
.
RawMessage
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
WeChatOAuthEnabled
bool
`json:"wechat_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"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
PaymentEnabled
bool
`json:"payment_enabled"`
PaymentEnabled
bool
`json:"payment_enabled"`
OIDCOAuthEnabled
bool
`json:"oidc_oauth_enabled"`
OIDCOAuthEnabled
bool
`json:"oidc_oauth_enabled"`
...
@@ -411,6 +417,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
...
@@ -411,6 +417,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
CustomEndpoints
:
safeRawJSONArray
(
settings
.
CustomEndpoints
),
CustomEndpoints
:
safeRawJSONArray
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
WeChatOAuthEnabled
:
settings
.
WeChatOAuthEnabled
,
WeChatOAuthEnabled
:
settings
.
WeChatOAuthEnabled
,
WeChatOAuthOpenEnabled
:
settings
.
WeChatOAuthOpenEnabled
,
WeChatOAuthMPEnabled
:
settings
.
WeChatOAuthMPEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
PaymentEnabled
:
settings
.
PaymentEnabled
,
PaymentEnabled
:
settings
.
PaymentEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
...
@@ -460,11 +468,17 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
...
@@ -460,11 +468,17 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
}
}
func
isWeChatOAuthConfigured
()
bool
{
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"
))
!=
""
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"
))
!=
""
strings
.
TrimSpace
(
os
.
Getenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
))
!=
""
return
openConfigured
||
mpConfigured
}
}
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
// 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
...
@@ -90,3 +90,18 @@ func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
settings
.
ForceEmailOnThirdPartySignup
)
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 {
...
@@ -161,13 +161,15 @@ type PublicSettings struct {
CustomMenuItems
string
// JSON array of custom menu items
CustomMenuItems
string
// JSON array of custom menu items
CustomEndpoints
string
// JSON array of custom endpoints
CustomEndpoints
string
// JSON array of custom endpoints
LinuxDoOAuthEnabled
bool
LinuxDoOAuthEnabled
bool
WeChatOAuthEnabled
bool
WeChatOAuthEnabled
bool
BackendModeEnabled
bool
WeChatOAuthOpenEnabled
bool
PaymentEnabled
bool
WeChatOAuthMPEnabled
bool
OIDCOAuthEnabled
bool
BackendModeEnabled
bool
OIDCOAuthProviderName
string
PaymentEnabled
bool
Version
string
OIDCOAuthEnabled
bool
OIDCOAuthProviderName
string
Version
string
BalanceLowNotifyEnabled
bool
BalanceLowNotifyEnabled
bool
AccountQuotaNotifyEnabled
bool
AccountQuotaNotifyEnabled
bool
...
...
frontend/src/api/auth.ts
View file @
067eb23d
...
@@ -363,7 +363,7 @@ export interface ResolvedWeChatOAuthStart {
...
@@ -363,7 +363,7 @@ export interface ResolvedWeChatOAuthStart {
unavailableReason
:
WeChatOAuthUnavailableReason
|
null
unavailableReason
:
WeChatOAuthUnavailableReason
|
null
}
}
type
WeChatOAuthPublicSettings
=
{
export
type
WeChatOAuthPublicSettings
=
{
wechat_oauth_enabled
?:
boolean
wechat_oauth_enabled
?:
boolean
wechat_oauth_open_enabled
?:
boolean
wechat_oauth_open_enabled
?:
boolean
wechat_oauth_mp_enabled
?:
boolean
wechat_oauth_mp_enabled
?:
boolean
...
...
frontend/src/api/user.ts
View file @
067eb23d
...
@@ -4,7 +4,11 @@
...
@@ -4,7 +4,11 @@
*/
*/
import
{
apiClient
}
from
'
./client
'
import
{
apiClient
}
from
'
./client
'
import
{
prepareOAuthBindAccessTokenCookie
}
from
'
./auth
'
import
{
prepareOAuthBindAccessTokenCookie
,
resolveWeChatOAuthStart
,
type
WeChatOAuthPublicSettings
,
}
from
'
./auth
'
import
type
{
User
,
ChangePasswordRequest
,
NotifyEmailEntry
,
UserAuthProvider
}
from
'
@/types
'
import
type
{
User
,
ChangePasswordRequest
,
NotifyEmailEntry
,
UserAuthProvider
}
from
'
@/types
'
/**
/**
...
@@ -89,6 +93,7 @@ export type BindableOAuthProvider = Exclude<UserAuthProvider, 'email'>
...
@@ -89,6 +93,7 @@ export type BindableOAuthProvider = Exclude<UserAuthProvider, 'email'>
interface
BuildOAuthBindingStartURLOptions
{
interface
BuildOAuthBindingStartURLOptions
{
redirectTo
?:
string
redirectTo
?:
string
wechatOAuthSettings
?:
WeChatOAuthPublicSettings
|
null
}
}
export
function
resolveWeChatOAuthMode
():
'
open
'
|
'
mp
'
{
export
function
resolveWeChatOAuthMode
():
'
open
'
|
'
mp
'
{
...
@@ -98,10 +103,19 @@ export function resolveWeChatOAuthMode(): 'open' | 'mp' {
...
@@ -98,10 +103,19 @@ export function resolveWeChatOAuthMode(): 'open' | 'mp' {
return
/MicroMessenger/i
.
test
(
navigator
.
userAgent
)
?
'
mp
'
:
'
open
'
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
(
export
function
buildOAuthBindingStartURL
(
provider
:
BindableOAuthProvider
,
provider
:
BindableOAuthProvider
,
options
:
BuildOAuthBindingStartURLOptions
=
{}
options
:
BuildOAuthBindingStartURLOptions
=
{}
):
string
{
):
string
|
null
{
const
redirectTo
=
options
.
redirectTo
?.
trim
()
||
'
/profile
'
const
redirectTo
=
options
.
redirectTo
?.
trim
()
||
'
/profile
'
const
apiBase
=
(
import
.
meta
.
env
.
VITE_API_BASE_URL
as
string
|
undefined
)
||
'
/api/v1
'
const
apiBase
=
(
import
.
meta
.
env
.
VITE_API_BASE_URL
as
string
|
undefined
)
||
'
/api/v1
'
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
...
@@ -111,7 +125,11 @@ export function buildOAuthBindingStartURL(
...
@@ -111,7 +125,11 @@ export function buildOAuthBindingStartURL(
})
})
if
(
provider
===
'
wechat
'
)
{
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
()}
`
return
`
${
normalized
}
/auth/oauth/
${
provider
}
/start?
${
params
.
toString
()}
`
...
@@ -124,8 +142,12 @@ export function startOAuthBinding(
...
@@ -124,8 +142,12 @@ export function startOAuthBinding(
if
(
typeof
window
===
'
undefined
'
)
{
if
(
typeof
window
===
'
undefined
'
)
{
return
return
}
}
const
startURL
=
buildOAuthBindingStartURL
(
provider
,
options
)
if
(
!
startURL
)
{
return
}
prepareOAuthBindAccessTokenCookie
()
prepareOAuthBindAccessTokenCookie
()
window
.
location
.
href
=
buildOAuthBindingStartURL
(
provider
,
options
)
window
.
location
.
href
=
startURL
}
}
export
const
userAPI
=
{
export
const
userAPI
=
{
...
...
frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue
View file @
067eb23d
...
@@ -52,7 +52,9 @@
...
@@ -52,7 +52,9 @@
import
{
computed
}
from
'
vue
'
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useRoute
}
from
'
vue-router
'
import
{
useRoute
}
from
'
vue-router
'
import
{
resolveWeChatOAuthStart
,
type
WeChatOAuthPublicSettings
}
from
'
@/api/auth
'
import
{
startOAuthBinding
}
from
'
@/api/user
'
import
{
startOAuthBinding
}
from
'
@/api/user
'
import
{
useAppStore
}
from
'
@/stores
'
import
type
{
User
,
UserAuthBindingStatus
,
UserAuthProvider
}
from
'
@/types
'
import
type
{
User
,
UserAuthBindingStatus
,
UserAuthProvider
}
from
'
@/types
'
const
props
=
withDefaults
(
const
props
=
withDefaults
(
...
@@ -62,17 +64,44 @@ const props = withDefaults(
...
@@ -62,17 +64,44 @@ const props = withDefaults(
oidcEnabled
?:
boolean
oidcEnabled
?:
boolean
oidcProviderName
?:
string
oidcProviderName
?:
string
wechatEnabled
?:
boolean
wechatEnabled
?:
boolean
wechatOpenEnabled
?:
boolean
wechatMpEnabled
?:
boolean
}
>
(),
}
>
(),
{
{
linuxdoEnabled
:
false
,
linuxdoEnabled
:
false
,
oidcEnabled
:
false
,
oidcEnabled
:
false
,
oidcProviderName
:
'
OIDC
'
,
oidcProviderName
:
'
OIDC
'
,
wechatEnabled
:
false
,
wechatEnabled
:
false
,
wechatOpenEnabled
:
undefined
,
wechatMpEnabled
:
undefined
,
}
}
)
)
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
route
=
useRoute
()
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
{
function
normalizeBindingStatus
(
binding
:
boolean
|
UserAuthBindingStatus
|
undefined
):
boolean
|
null
{
if
(
typeof
binding
===
'
boolean
'
)
{
if
(
typeof
binding
===
'
boolean
'
)
{
...
@@ -129,7 +158,7 @@ const providerItems = computed(() => [
...
@@ -129,7 +158,7 @@ const providerItems = computed(() => [
provider
:
'
wechat
'
as
const
,
provider
:
'
wechat
'
as
const
,
label
:
t
(
'
profile.authBindings.providers.wechat
'
),
label
:
t
(
'
profile.authBindings.providers.wechat
'
),
bound
:
getBindingStatus
(
'
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 {
...
@@ -139,6 +168,7 @@ function startBinding(provider: UserAuthProvider): void {
}
}
startOAuthBinding
(
provider
,
{
startOAuthBinding
(
provider
,
{
redirectTo
:
route
.
fullPath
||
'
/profile
'
,
redirectTo
:
route
.
fullPath
||
'
/profile
'
,
wechatOAuthSettings
:
provider
===
'
wechat
'
?
wechatOAuthSettings
.
value
:
null
,
}
)
}
)
}
}
<
/script
>
<
/script
>
frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts
View file @
067eb23d
import
{
mount
}
from
'
@vue/test-utils
'
import
{
mount
}
from
'
@vue/test-utils
'
import
{
createPinia
,
setActivePinia
}
from
'
pinia
'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
ProfileIdentityBindingsSection
from
'
@/components/user/profile/ProfileIdentityBindingsSection.vue
'
import
ProfileIdentityBindingsSection
from
'
@/components/user/profile/ProfileIdentityBindingsSection.vue
'
import
{
useAppStore
}
from
'
@/stores
'
import
type
{
User
}
from
'
@/types
'
import
type
{
User
}
from
'
@/types
'
const
routeState
=
vi
.
hoisted
(()
=>
({
const
routeState
=
vi
.
hoisted
(()
=>
({
...
@@ -11,6 +13,8 @@ const locationState = vi.hoisted(() => ({
...
@@ -11,6 +13,8 @@ const locationState = vi.hoisted(() => ({
current
:
{
href
:
'
http://localhost/profile
'
}
as
{
href
:
string
},
current
:
{
href
:
'
http://localhost/profile
'
}
as
{
href
:
string
},
}))
}))
let
pinia
:
ReturnType
<
typeof
createPinia
>
vi
.
mock
(
'
vue-router
'
,
()
=>
({
vi
.
mock
(
'
vue-router
'
,
()
=>
({
useRoute
:
()
=>
routeState
,
useRoute
:
()
=>
routeState
,
}))
}))
...
@@ -57,6 +61,8 @@ function createUser(overrides: Partial<User> = {}): User {
...
@@ -57,6 +61,8 @@ function createUser(overrides: Partial<User> = {}): User {
describe
(
'
ProfileIdentityBindingsSection
'
,
()
=>
{
describe
(
'
ProfileIdentityBindingsSection
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
pinia
=
createPinia
()
setActivePinia
(
pinia
)
routeState
.
fullPath
=
'
/profile
'
routeState
.
fullPath
=
'
/profile
'
locationState
.
current
=
{
href
:
'
http://localhost/profile
'
}
locationState
.
current
=
{
href
:
'
http://localhost/profile
'
}
Object
.
defineProperty
(
window
,
'
location
'
,
{
Object
.
defineProperty
(
window
,
'
location
'
,
{
...
@@ -67,6 +73,9 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -67,6 +73,9 @@ describe('ProfileIdentityBindingsSection', () => {
configurable
:
true
,
configurable
:
true
,
value
:
'
Mozilla/5.0
'
,
value
:
'
Mozilla/5.0
'
,
})
})
const
appStore
=
useAppStore
()
appStore
.
cachedPublicSettings
=
null
appStore
.
publicSettingsLoaded
=
false
})
})
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -75,6 +84,9 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -75,6 +84,9 @@ describe('ProfileIdentityBindingsSection', () => {
it
(
'
renders provider binding states and provider-specific bind actions
'
,
()
=>
{
it
(
'
renders provider binding states and provider-specific bind actions
'
,
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
props
:
{
user
:
createUser
({
user
:
createUser
({
auth_bindings
:
{
auth_bindings
:
{
...
@@ -102,11 +114,16 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -102,11 +114,16 @@ describe('ProfileIdentityBindingsSection', () => {
it
(
'
starts the WeChat bind flow for the current profile page
'
,
async
()
=>
{
it
(
'
starts the WeChat bind flow for the current profile page
'
,
async
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
props
:
{
user
:
createUser
(),
user
:
createUser
(),
linuxdoEnabled
:
false
,
linuxdoEnabled
:
false
,
oidcEnabled
:
false
,
oidcEnabled
:
false
,
wechatEnabled
:
true
,
wechatEnabled
:
true
,
wechatOpenEnabled
:
true
,
wechatMpEnabled
:
false
,
},
},
})
})
...
@@ -117,4 +134,22 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -117,4 +134,22 @@ describe('ProfileIdentityBindingsSection', () => {
expect
(
locationState
.
current
.
href
).
toContain
(
'
intent=bind_current_user
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
intent=bind_current_user
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
redirect=%2Fprofile
'
)
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', () => {
...
@@ -338,6 +338,8 @@ export const useAppStore = defineStore('app', () => {
custom_endpoints
:
[],
custom_endpoints
:
[],
linuxdo_oauth_enabled
:
false
,
linuxdo_oauth_enabled
:
false
,
wechat_oauth_enabled
:
false
,
wechat_oauth_enabled
:
false
,
wechat_oauth_open_enabled
:
false
,
wechat_oauth_mp_enabled
:
false
,
oidc_oauth_enabled
:
false
,
oidc_oauth_enabled
:
false
,
oidc_oauth_provider_name
:
'
OIDC
'
,
oidc_oauth_provider_name
:
'
OIDC
'
,
backend_mode_enabled
:
false
,
backend_mode_enabled
:
false
,
...
...
frontend/src/types/index.ts
View file @
067eb23d
...
@@ -165,6 +165,8 @@ export interface PublicSettings {
...
@@ -165,6 +165,8 @@ export interface PublicSettings {
custom_endpoints
:
CustomEndpoint
[]
custom_endpoints
:
CustomEndpoint
[]
linuxdo_oauth_enabled
:
boolean
linuxdo_oauth_enabled
:
boolean
wechat_oauth_enabled
:
boolean
wechat_oauth_enabled
:
boolean
wechat_oauth_open_enabled
?:
boolean
wechat_oauth_mp_enabled
?:
boolean
oidc_oauth_enabled
:
boolean
oidc_oauth_enabled
:
boolean
oidc_oauth_provider_name
:
string
oidc_oauth_provider_name
:
string
backend_mode_enabled
:
boolean
backend_mode_enabled
:
boolean
...
...
frontend/src/views/auth/WechatCallbackView.vue
View file @
067eb23d
...
@@ -297,6 +297,7 @@ import {
...
@@ -297,6 +297,7 @@ import {
login2FA
,
login2FA
,
prepareOAuthBindAccessTokenCookie
,
prepareOAuthBindAccessTokenCookie
,
persistOAuthTokenContext
,
persistOAuthTokenContext
,
resolveWeChatOAuthStart
,
type
OAuthAdoptionDecision
,
type
OAuthAdoptionDecision
,
type
PendingOAuthExchangeResponse
type
PendingOAuthExchangeResponse
}
from
'
@/api/auth
'
}
from
'
@/api/auth
'
...
@@ -378,7 +379,47 @@ function normalizeWeChatOAuthMode(value: unknown): 'open' | 'mp' | null {
...
@@ -378,7 +379,47 @@ function normalizeWeChatOAuthMode(value: unknown): 'open' | 'mp' | null {
return
value
===
'
open
'
||
value
===
'
mp
'
?
value
:
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
)
const
queryMode
=
normalizeWeChatOAuthMode
(
route
.
query
.
mode
)
return
queryMode
||
resolveWeChatOAuthMode
()
return
queryMode
||
resolveWeChatOAuthMode
()
}
}
...
@@ -389,11 +430,15 @@ function resolveRedirectTarget(): string {
...
@@ -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
apiBase
=
(
import
.
meta
.
env
.
VITE_API_BASE_URL
as
string
|
undefined
)
||
'
/api/v1
'
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
const
mode
=
resolveRequestedWeChatOAuthMode
()
if
(
!
mode
)
{
return
null
}
const
params
=
new
URLSearchParams
({
const
params
=
new
URLSearchParams
({
mode
:
resolveRequestedWeChatOAuthMode
()
,
mode
,
redirect
:
resolveRedirectTarget
(),
redirect
:
resolveRedirectTarget
(),
intent
,
intent
,
}
)
}
)
...
@@ -406,11 +451,15 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use
...
@@ -406,11 +451,15 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use
return
`${normalized
}
/auth/oauth/wechat/start?${params.toString()
}
`
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
({
const
params
=
new
URLSearchParams
({
wechat_bind_existing
:
'
1
'
,
wechat_bind_existing
:
'
1
'
,
redirect
:
resolveRedirectTarget
(),
redirect
:
resolveRedirectTarget
(),
mode
:
resolveRequestedWeChatOAuthMode
()
,
mode
,
}
)
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
const
email
=
existingAccountEmail
.
value
.
trim
()
...
@@ -444,14 +493,31 @@ function serializeAdoptionDecision(decision: OAuthAdoptionDecision): Record<stri
...
@@ -444,14 +493,31 @@ function serializeAdoptionDecision(decision: OAuthAdoptionDecision): Record<stri
}
}
async
function
handleExistingAccountBinding
()
{
async
function
handleExistingAccountBinding
()
{
const
unavailableMessage
=
resolveConfiguredWeChatOAuthMode
()
===
null
?
resolveWeChatOAuthUnavailableMessage
()
:
''
if
(
getAuthToken
())
{
if
(
getAuthToken
())
{
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
if
(
!
startURL
)
{
errorMessage
.
value
=
unavailableMessage
||
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
return
}
prepareOAuthBindAccessTokenCookie
()
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
return
}
}
const
params
=
new
URLSearchParams
({
const
params
=
new
URLSearchParams
({
redirect
:
buildExistingAccountR
esumePath
()
,
redirect
:
r
esumePath
,
}
)
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
const
email
=
existingAccountEmail
.
value
.
trim
()
if
(
email
)
{
if
(
email
)
{
...
@@ -720,19 +786,36 @@ async function handleSubmitTotpChallenge() {
...
@@ -720,19 +786,36 @@ async function handleSubmitTotpChallenge() {
}
}
onMounted
(
async
()
=>
{
onMounted
(
async
()
=>
{
await
ensurePublicSettingsLoaded
()
if
(
typeof
route
.
query
.
email
===
'
string
'
)
{
if
(
typeof
route
.
query
.
email
===
'
string
'
)
{
existingAccountEmail
.
value
=
route
.
query
.
email
existingAccountEmail
.
value
=
route
.
query
.
email
}
}
if
(
route
.
query
.
wechat_bind_existing
===
'
1
'
)
{
if
(
route
.
query
.
wechat_bind_existing
===
'
1
'
)
{
if
(
getAuthToken
())
{
if
(
getAuthToken
())
{
const
startURL
=
resolveWeChatStartURL
(
'
bind_current_user
'
)
if
(
!
startURL
)
{
errorMessage
.
value
=
resolveWeChatOAuthUnavailableMessage
()
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
return
}
prepareOAuthBindAccessTokenCookie
()
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
return
}
}
const
params
=
new
URLSearchParams
({
const
params
=
new
URLSearchParams
({
redirect
:
buildExistingAccountR
esumePath
()
,
redirect
:
r
esumePath
,
}
)
}
)
const
email
=
existingAccountEmail
.
value
.
trim
()
const
email
=
existingAccountEmail
.
value
.
trim
()
if
(
email
)
{
if
(
email
)
{
...
...
frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
View file @
067eb23d
...
@@ -14,8 +14,10 @@ const {
...
@@ -14,8 +14,10 @@ const {
setTokenMock
,
setTokenMock
,
showSuccessMock
,
showSuccessMock
,
showErrorMock
,
showErrorMock
,
fetchPublicSettingsMock
,
routeState
,
routeState
,
locationState
,
locationState
,
appStoreState
,
}
=
vi
.
hoisted
(()
=>
({
}
=
vi
.
hoisted
(()
=>
({
exchangePendingOAuthCompletionMock
:
vi
.
fn
(),
exchangePendingOAuthCompletionMock
:
vi
.
fn
(),
completeWeChatOAuthRegistrationMock
:
vi
.
fn
(),
completeWeChatOAuthRegistrationMock
:
vi
.
fn
(),
...
@@ -28,6 +30,7 @@ const {
...
@@ -28,6 +30,7 @@ const {
setTokenMock
:
vi
.
fn
(),
setTokenMock
:
vi
.
fn
(),
showSuccessMock
:
vi
.
fn
(),
showSuccessMock
:
vi
.
fn
(),
showErrorMock
:
vi
.
fn
(),
showErrorMock
:
vi
.
fn
(),
fetchPublicSettingsMock
:
vi
.
fn
(),
routeState
:
{
routeState
:
{
query
:
{}
as
Record
<
string
,
unknown
>
,
query
:
{}
as
Record
<
string
,
unknown
>
,
},
},
...
@@ -39,6 +42,10 @@ const {
...
@@ -39,6 +42,10 @@ const {
pathname
:
'
/auth/wechat/callback
'
pathname
:
'
/auth/wechat/callback
'
}
as
{
href
:
string
;
hash
:
string
;
search
:
string
;
pathname
:
string
},
}
as
{
href
:
string
;
hash
:
string
;
search
:
string
;
pathname
:
string
},
},
},
appStoreState
:
{
cachedPublicSettings
:
null
as
null
|
Record
<
string
,
unknown
>
,
publicSettingsLoaded
:
false
,
},
}))
}))
vi
.
mock
(
'
vue-router
'
,
()
=>
({
vi
.
mock
(
'
vue-router
'
,
()
=>
({
...
@@ -102,8 +109,10 @@ vi.mock('@/stores', () => ({
...
@@ -102,8 +109,10 @@ vi.mock('@/stores', () => ({
setToken
:
setTokenMock
,
setToken
:
setTokenMock
,
}),
}),
useAppStore
:
()
=>
({
useAppStore
:
()
=>
({
...
appStoreState
,
showSuccess
:
showSuccessMock
,
showSuccess
:
showSuccessMock
,
showError
:
showErrorMock
,
showError
:
showErrorMock
,
fetchPublicSettings
:
fetchPublicSettingsMock
,
}),
}),
}))
}))
...
@@ -139,7 +148,10 @@ describe('WechatCallbackView', () => {
...
@@ -139,7 +148,10 @@ describe('WechatCallbackView', () => {
showErrorMock
.
mockReset
()
showErrorMock
.
mockReset
()
prepareOAuthBindAccessTokenCookieMock
.
mockReset
()
prepareOAuthBindAccessTokenCookieMock
.
mockReset
()
getAuthTokenMock
.
mockReset
()
getAuthTokenMock
.
mockReset
()
fetchPublicSettingsMock
.
mockReset
()
routeState
.
query
=
{}
routeState
.
query
=
{}
appStoreState
.
cachedPublicSettings
=
null
appStoreState
.
publicSettingsLoaded
=
false
localStorage
.
clear
()
localStorage
.
clear
()
locationState
.
current
=
{
locationState
.
current
=
{
href
:
'
http://localhost/auth/wechat/callback
'
,
href
:
'
http://localhost/auth/wechat/callback
'
,
...
@@ -157,6 +169,38 @@ describe('WechatCallbackView', () => {
...
@@ -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
()
=>
{
it
(
'
does not send adoption decisions during the initial exchange
'
,
async
()
=>
{
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
access_token
:
'
access-token
'
,
access_token
:
'
access-token
'
,
...
...
frontend/src/views/user/ProfileView.vue
View file @
067eb23d
...
@@ -67,7 +67,6 @@
...
@@ -67,7 +67,6 @@
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
h
,
onMounted
,
ref
}
from
'
vue
'
import
{
computed
,
h
,
onMounted
,
ref
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
authAPI
}
from
'
@/api
'
import
{
Icon
}
from
'
@/components/icons
'
import
{
Icon
}
from
'
@/components/icons
'
import
StatCard
from
'
@/components/common/StatCard.vue
'
import
StatCard
from
'
@/components/common/StatCard.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
...
@@ -76,10 +75,12 @@ import ProfileEditForm from '@/components/user/profile/ProfileEditForm.vue'
...
@@ -76,10 +75,12 @@ import ProfileEditForm from '@/components/user/profile/ProfileEditForm.vue'
import
ProfileInfoCard
from
'
@/components/user/profile/ProfileInfoCard.vue
'
import
ProfileInfoCard
from
'
@/components/user/profile/ProfileInfoCard.vue
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
import
ProfileTotpCard
from
'
@/components/user/profile/ProfileTotpCard.vue
'
import
ProfileTotpCard
from
'
@/components/user/profile/ProfileTotpCard.vue
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
formatDate
}
from
'
@/utils/format
'
import
{
formatDate
}
from
'
@/utils/format
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
user
=
computed
(()
=>
authStore
.
user
)
const
user
=
computed
(()
=>
authStore
.
user
)
...
@@ -121,8 +122,11 @@ onMounted(async () => {
...
@@ -121,8 +122,11 @@ onMounted(async () => {
console
.
error
(
'
Failed to refresh profile:
'
,
error
)
console
.
error
(
'
Failed to refresh profile:
'
,
error
)
})
})
const
settingsLoad
=
a
uthAPI
.
get
PublicSettings
()
const
settingsLoad
=
a
ppStore
.
fetch
PublicSettings
()
.
then
((
settings
)
=>
{
.
then
((
settings
)
=>
{
if
(
!
settings
)
{
return
}
contactInfo
.
value
=
settings
.
contact_info
||
''
contactInfo
.
value
=
settings
.
contact_info
||
''
balanceLowNotifyEnabled
.
value
=
settings
.
balance_low_notify_enabled
??
false
balanceLowNotifyEnabled
.
value
=
settings
.
balance_low_notify_enabled
??
false
systemDefaultThreshold
.
value
=
settings
.
balance_low_notify_threshold
??
0
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