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
"backend/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "ed8a9d975b5c4b4d42893305e07adb548db2683a"
Commit
067eb23d
authored
Apr 21, 2026
by
IanShaw027
Browse files
Tighten WeChat OAuth capability mode selection
parent
12f4af74
Changes
15
Show 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
...
@@ -163,6 +163,8 @@ type PublicSettings struct {
...
@@ -163,6 +163,8 @@ type PublicSettings struct {
LinuxDoOAuthEnabled
bool
LinuxDoOAuthEnabled
bool
WeChatOAuthEnabled
bool
WeChatOAuthEnabled
bool
WeChatOAuthOpenEnabled
bool
WeChatOAuthMPEnabled
bool
BackendModeEnabled
bool
BackendModeEnabled
bool
PaymentEnabled
bool
PaymentEnabled
bool
OIDCOAuthEnabled
bool
OIDCOAuthEnabled
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