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
55513493
"backend/vscode:/vscode.git/clone" did not exist on "ca68839bf03556834896ba53362c78a0c98988b5"
Commit
55513493
authored
Apr 22, 2026
by
IanShaw027
Browse files
fix: clean up profile auth binding notes
parent
c6d25f69
Changes
8
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/user_handler_test.go
View file @
55513493
...
@@ -323,6 +323,11 @@ func TestUserHandlerGetProfileReturnsLegacyCompatibilityFields(t *testing.T) {
...
@@ -323,6 +323,11 @@ func TestUserHandlerGetProfileReturnsLegacyCompatibilityFields(t *testing.T) {
emailBinding
,
ok
:=
identityBindings
[
"email"
]
.
(
map
[
string
]
any
)
emailBinding
,
ok
:=
identityBindings
[
"email"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
true
,
emailBinding
[
"bound"
])
require
.
Equal
(
t
,
true
,
emailBinding
[
"bound"
])
require
.
Equal
(
t
,
"profile.authBindings.notes.emailManagedFromProfile"
,
emailBinding
[
"note_key"
])
linuxdoCompatBinding
,
ok
:=
identityBindings
[
"linuxdo"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
"profile.authBindings.notes.canUnbind"
,
linuxdoCompatBinding
[
"note_key"
])
profileSources
,
ok
:=
resp
.
Data
[
"profile_sources"
]
.
(
map
[
string
]
any
)
profileSources
,
ok
:=
resp
.
Data
[
"profile_sources"
]
.
(
map
[
string
]
any
)
require
.
True
(
t
,
ok
)
require
.
True
(
t
,
ok
)
...
...
backend/internal/server/api_contract_test.go
View file @
55513493
...
@@ -77,6 +77,7 @@ func TestAPIContracts(t *testing.T) {
...
@@ -77,6 +77,7 @@ func TestAPIContracts(t *testing.T) {
"can_unbind": false,
"can_unbind": false,
"display_name": "alice@example.com",
"display_name": "alice@example.com",
"subject_hint": "a***e@example.com",
"subject_hint": "a***e@example.com",
"note_key": "profile.authBindings.notes.emailManagedFromProfile",
"note": "Primary account email is managed from the profile form."
"note": "Primary account email is managed from the profile form."
},
},
"linuxdo": {
"linuxdo": {
...
@@ -114,6 +115,7 @@ func TestAPIContracts(t *testing.T) {
...
@@ -114,6 +115,7 @@ func TestAPIContracts(t *testing.T) {
"can_unbind": false,
"can_unbind": false,
"display_name": "alice@example.com",
"display_name": "alice@example.com",
"subject_hint": "a***e@example.com",
"subject_hint": "a***e@example.com",
"note_key": "profile.authBindings.notes.emailManagedFromProfile",
"note": "Primary account email is managed from the profile form."
"note": "Primary account email is managed from the profile form."
},
},
"linuxdo": {
"linuxdo": {
...
@@ -151,6 +153,7 @@ func TestAPIContracts(t *testing.T) {
...
@@ -151,6 +153,7 @@ func TestAPIContracts(t *testing.T) {
"can_unbind": false,
"can_unbind": false,
"display_name": "alice@example.com",
"display_name": "alice@example.com",
"subject_hint": "a***e@example.com",
"subject_hint": "a***e@example.com",
"note_key": "profile.authBindings.notes.emailManagedFromProfile",
"note": "Primary account email is managed from the profile form."
"note": "Primary account email is managed from the profile form."
},
},
"linuxdo": {
"linuxdo": {
...
...
backend/internal/service/user_service.go
View file @
55513493
...
@@ -134,6 +134,7 @@ type UserIdentitySummary struct {
...
@@ -134,6 +134,7 @@ type UserIdentitySummary struct {
BindStartPath
string
`json:"bind_start_path,omitempty"`
BindStartPath
string
`json:"bind_start_path,omitempty"`
CanBind
bool
`json:"can_bind"`
CanBind
bool
`json:"can_bind"`
CanUnbind
bool
`json:"can_unbind"`
CanUnbind
bool
`json:"can_unbind"`
NoteKey
string
`json:"note_key,omitempty"`
Note
string
`json:"note,omitempty"`
Note
string
`json:"note,omitempty"`
}
}
...
@@ -156,6 +157,12 @@ type StartUserIdentityBindingResult struct {
...
@@ -156,6 +157,12 @@ type StartUserIdentityBindingResult struct {
UseBrowserRedirect
bool
`json:"use_browser_redirect"`
UseBrowserRedirect
bool
`json:"use_browser_redirect"`
}
}
const
(
userIdentityNoteEmailManagedFromProfile
=
"profile.authBindings.notes.emailManagedFromProfile"
userIdentityNoteCanUnbind
=
"profile.authBindings.notes.canUnbind"
userIdentityNoteBindAnotherBeforeUnbind
=
"profile.authBindings.notes.bindAnotherBeforeUnbind"
)
// UpdateProfileRequest 更新用户资料请求
// UpdateProfileRequest 更新用户资料请求
type
UpdateProfileRequest
struct
{
type
UpdateProfileRequest
struct
{
Email
*
string
`json:"email"`
Email
*
string
`json:"email"`
...
@@ -601,6 +608,7 @@ func (s *UserService) buildEmailIdentitySummary(user *User, records []UserAuthId
...
@@ -601,6 +608,7 @@ func (s *UserService) buildEmailIdentitySummary(user *User, records []UserAuthId
Provider
:
"email"
,
Provider
:
"email"
,
CanBind
:
false
,
CanBind
:
false
,
CanUnbind
:
false
,
CanUnbind
:
false
,
NoteKey
:
userIdentityNoteEmailManagedFromProfile
,
Note
:
"Primary account email is managed from the profile form."
,
Note
:
"Primary account email is managed from the profile form."
,
}
}
if
user
==
nil
{
if
user
==
nil
{
...
@@ -668,8 +676,10 @@ func (s *UserService) buildProviderIdentitySummary(provider string, user *User,
...
@@ -668,8 +676,10 @@ func (s *UserService) buildProviderIdentitySummary(provider string, user *User,
summary
.
VerifiedAt
=
primary
.
VerifiedAt
summary
.
VerifiedAt
=
primary
.
VerifiedAt
summary
.
CanUnbind
=
s
.
canUnbindProvider
(
provider
,
user
,
records
)
summary
.
CanUnbind
=
s
.
canUnbindProvider
(
provider
,
user
,
records
)
if
summary
.
CanUnbind
{
if
summary
.
CanUnbind
{
summary
.
NoteKey
=
userIdentityNoteCanUnbind
summary
.
Note
=
"You can unbind this sign-in method."
summary
.
Note
=
"You can unbind this sign-in method."
}
else
{
}
else
{
summary
.
NoteKey
=
userIdentityNoteBindAnotherBeforeUnbind
summary
.
Note
=
"Bind another sign-in method before unbinding."
summary
.
Note
=
"Bind another sign-in method before unbinding."
}
}
return
summary
return
summary
...
...
frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue
View file @
55513493
...
@@ -67,23 +67,23 @@
...
@@ -67,23 +67,23 @@
</p>
</p>
<div
<div
v-if=
"
item.details && (item.details.display_name || item.details.subject_hint || bindingCountLabel(item.details) ||
item.details
.note
)"
v-if=
"
hasBindingDetails(item.provider,
item.details)"
class=
"grid gap-1 text-sm text-gray-500 dark:text-gray-400"
class=
"grid gap-1 text-sm text-gray-500 dark:text-gray-400"
>
>
<p
<p
v-if=
"item.details.display_name"
v-if=
"
item.provider !== 'email' &&
item.details
?
.display_name"
class=
"font-medium text-gray-700 dark:text-gray-200"
class=
"font-medium text-gray-700 dark:text-gray-200"
>
>
{{
item
.
details
.
display_name
}}
{{
item
.
details
.
display_name
}}
</p>
</p>
<p
v-if=
"item.details.subject_hint"
>
<p
v-if=
"
item.provider !== 'email' &&
item.details
?
.subject_hint"
>
{{
item
.
details
.
subject_hint
}}
{{
item
.
details
.
subject_hint
}}
</p>
</p>
<p
v-if=
"bindingCountLabel(item.details)"
>
<p
v-if=
"bindingCountLabel(item.details)"
>
{{
bindingCountLabel
(
item
.
details
)
}}
{{
bindingCountLabel
(
item
.
details
)
}}
</p>
</p>
<p
v-if=
"item.details
.note
"
>
<p
v-if=
"
bindingNote(
item.details
)
"
>
{{
item
.
details
.
note
}}
{{
bindingNote
(
item
.
details
)
}}
</p>
</p>
</div>
</div>
...
@@ -298,6 +298,13 @@ const emailSubmitActionLabel = computed(() =>
...
@@ -298,6 +298,13 @@ const emailSubmitActionLabel = computed(() =>
?
t
(
'
profile.authBindings.confirmEmailReplaceAction
'
)
?
t
(
'
profile.authBindings.confirmEmailReplaceAction
'
)
:
t
(
'
profile.authBindings.confirmEmailBindAction
'
)
:
t
(
'
profile.authBindings.confirmEmailBindAction
'
)
)
)
const
legacyBindingNoteKeys
:
Record
<
string
,
string
>
=
{
'
Primary account email is managed from the profile form.
'
:
'
profile.authBindings.notes.emailManagedFromProfile
'
,
'
You can unbind this sign-in method.
'
:
'
profile.authBindings.notes.canUnbind
'
,
'
Bind another sign-in method before unbinding.
'
:
'
profile.authBindings.notes.bindAnotherBeforeUnbind
'
,
}
function
resolveLegacyCompatibleWeChatSettings
(
function
resolveLegacyCompatibleWeChatSettings
(
settings
:
WeChatOAuthPublicSettings
|
null
|
undefined
settings
:
WeChatOAuthPublicSettings
|
null
|
undefined
...
@@ -489,6 +496,36 @@ function bindingCountLabel(details: UserAuthBindingStatus | null): string {
...
@@ -489,6 +496,36 @@ function bindingCountLabel(details: UserAuthBindingStatus | null): string {
return
t
(
'
profile.authBindings.boundCount
'
,
{
count
:
details
.
bound_count
}
)
return
t
(
'
profile.authBindings.boundCount
'
,
{
count
:
details
.
bound_count
}
)
}
}
function
bindingNote
(
details
:
UserAuthBindingStatus
|
null
):
string
{
if
(
!
details
)
{
return
''
}
const
noteKey
=
details
.
note_key
?.
trim
()
||
legacyBindingNoteKeys
[
details
.
note
?.
trim
()
||
''
]
||
''
if
(
noteKey
)
{
const
translated
=
t
(
noteKey
)
if
(
translated
!==
noteKey
)
{
return
translated
}
}
return
details
.
note
?.
trim
()
||
''
}
function
hasBindingDetails
(
provider
:
UserAuthProvider
,
details
:
UserAuthBindingStatus
|
null
):
boolean
{
if
(
!
details
)
{
return
false
}
const
showsProviderIdentityDetails
=
provider
!==
'
email
'
&&
Boolean
(
details
.
display_name
||
details
.
subject_hint
)
return
Boolean
(
showsProviderIdentityDetails
||
bindingCountLabel
(
details
)
||
bindingNote
(
details
))
}
function
toggleEmailForm
():
void
{
function
toggleEmailForm
():
void
{
isEmailFormExpanded
.
value
=
!
isEmailFormExpanded
.
value
isEmailFormExpanded
.
value
=
!
isEmailFormExpanded
.
value
}
}
...
...
frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts
View file @
55513493
...
@@ -64,6 +64,12 @@ vi.mock('vue-i18n', async (importOriginal) => {
...
@@ -64,6 +64,12 @@ vi.mock('vue-i18n', async (importOriginal) => {
if
(
key
===
'
profile.authBindings.codeSentTo
'
)
return
`Code sent to
${
params
?.
email
||
''
}
`
.
trim
()
if
(
key
===
'
profile.authBindings.codeSentTo
'
)
return
`Code sent to
${
params
?.
email
||
''
}
`
.
trim
()
if
(
key
===
'
profile.authBindings.bindSuccess
'
)
return
'
Bind success
'
if
(
key
===
'
profile.authBindings.bindSuccess
'
)
return
'
Bind success
'
if
(
key
===
'
profile.authBindings.replaceSuccess
'
)
return
'
Primary email updated
'
if
(
key
===
'
profile.authBindings.replaceSuccess
'
)
return
'
Primary email updated
'
if
(
key
===
'
profile.authBindings.notes.emailManagedFromProfile
'
)
return
'
Primary email is managed in the profile form
'
if
(
key
===
'
profile.authBindings.notes.canUnbind
'
)
return
'
You can unbind this sign-in method
'
if
(
key
===
'
profile.authBindings.notes.bindAnotherBeforeUnbind
'
)
return
'
Bind another sign-in method before unbinding
'
return
key
return
key
},
},
}),
}),
...
@@ -164,7 +170,7 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -164,7 +170,7 @@ describe('ProfileIdentityBindingsSection', () => {
await
wrapper
.
get
(
'
[data-testid="profile-binding-wechat-action"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="profile-binding-wechat-action"]
'
).
trigger
(
'
click
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
/api/v1/auth/oauth/wechat/start?
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
/api/v1/auth/oauth/wechat/
bind/
start?
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
mode=open
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
mode=open
'
)
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
'
)
...
@@ -219,7 +225,7 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -219,7 +225,7 @@ describe('ProfileIdentityBindingsSection', () => {
await
wrapper
.
get
(
'
[data-testid="profile-binding-wechat-action"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="profile-binding-wechat-action"]
'
).
trigger
(
'
click
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
/api/v1/auth/oauth/wechat/start?
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
/api/v1/auth/oauth/wechat/
bind/
start?
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
mode=open
'
)
expect
(
locationState
.
current
.
href
).
toContain
(
'
mode=open
'
)
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
'
)
...
@@ -401,6 +407,36 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -401,6 +407,36 @@ describe('ProfileIdentityBindingsSection', () => {
expect
(
wrapper
.
get
(
'
[data-testid="profile-binding-email-status"]
'
).
text
()).
toBe
(
'
Not bound
'
)
expect
(
wrapper
.
get
(
'
[data-testid="profile-binding-email-status"]
'
).
text
()).
toBe
(
'
Not bound
'
)
})
})
it
(
'
shows the bound email only once and localizes the email management note
'
,
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
user
:
createUser
({
email
:
'
alice@example.com
'
,
email_bound
:
true
,
auth_bindings
:
{
email
:
{
bound
:
true
,
display_name
:
'
alice@example.com
'
,
subject_hint
:
'
a***e@example.com
'
,
note_key
:
'
profile.authBindings.notes.emailManagedFromProfile
'
,
note
:
'
Primary account email is managed from the profile form.
'
,
}
as
any
,
},
}),
linuxdoEnabled
:
false
,
oidcEnabled
:
false
,
wechatEnabled
:
false
,
},
})
expect
(
wrapper
.
text
().
match
(
/alice@example
\.
com/g
)).
toHaveLength
(
1
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
a***e@example.com
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
Primary email is managed in the profile form
'
)
})
it
(
'
keeps the email form available for replacing a bound primary email
'
,
async
()
=>
{
it
(
'
keeps the email form available for replacing a bound primary email
'
,
async
()
=>
{
userApiMocks
.
sendEmailBindingCode
.
mockResolvedValue
(
undefined
)
userApiMocks
.
sendEmailBindingCode
.
mockResolvedValue
(
undefined
)
userApiMocks
.
bindEmailIdentity
.
mockResolvedValue
(
userApiMocks
.
bindEmailIdentity
.
mockResolvedValue
(
...
@@ -541,6 +577,36 @@ describe('ProfileIdentityBindingsSection', () => {
...
@@ -541,6 +577,36 @@ describe('ProfileIdentityBindingsSection', () => {
expect
(
wrapper
.
get
(
'
[data-testid="profile-binding-linuxdo-status"]
'
).
text
()).
toBe
(
'
Not bound
'
)
expect
(
wrapper
.
get
(
'
[data-testid="profile-binding-linuxdo-status"]
'
).
text
()).
toBe
(
'
Not bound
'
)
})
})
it
(
'
localizes third-party unbind guidance from note_key
'
,
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
plugins
:
[
pinia
],
},
props
:
{
user
:
createUser
({
email_bound
:
true
,
linuxdo_bound
:
true
,
auth_bindings
:
{
email
:
{
bound
:
true
},
linuxdo
:
{
bound
:
true
,
display_name
:
'
linuxdo-handle
'
,
note_key
:
'
profile.authBindings.notes.canUnbind
'
,
note
:
'
You can unbind this sign-in method.
'
,
can_unbind
:
true
,
}
as
any
,
},
}),
linuxdoEnabled
:
true
,
oidcEnabled
:
false
,
wechatEnabled
:
false
,
},
})
expect
(
wrapper
.
text
()).
toContain
(
'
You can unbind this sign-in method
'
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
You can unbind this sign-in method.
'
)
})
it
(
'
hides bind actions when provider details say bindable but the provider is disabled
'
,
()
=>
{
it
(
'
hides bind actions when provider details say bindable but the provider is disabled
'
,
()
=>
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
const
wrapper
=
mount
(
ProfileIdentityBindingsSection
,
{
global
:
{
global
:
{
...
...
frontend/src/i18n/locales/en.ts
View file @
55513493
...
@@ -1042,6 +1042,11 @@ export default {
...
@@ -1042,6 +1042,11 @@ export default {
oidc
:
'
{providerName}
'
,
oidc
:
'
{providerName}
'
,
wechat
:
'
WeChat
'
,
wechat
:
'
WeChat
'
,
},
},
notes
:
{
emailManagedFromProfile
:
'
Primary email is managed in the profile form
'
,
canUnbind
:
'
You can unbind this sign-in method
'
,
bindAnotherBeforeUnbind
:
'
Bind another sign-in method before unbinding
'
,
},
source
:
{
source
:
{
avatar
:
'
Avatar is currently synced from {providerName}
'
,
avatar
:
'
Avatar is currently synced from {providerName}
'
,
username
:
'
Nickname is currently synced from {providerName}
'
,
username
:
'
Nickname is currently synced from {providerName}
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
55513493
...
@@ -1046,6 +1046,11 @@ export default {
...
@@ -1046,6 +1046,11 @@ export default {
oidc
:
'
{providerName}
'
,
oidc
:
'
{providerName}
'
,
wechat
:
'
微信
'
,
wechat
:
'
微信
'
,
},
},
notes
:
{
emailManagedFromProfile
:
'
主邮箱在资料表单中管理
'
,
canUnbind
:
'
你可以解绑这个登录方式。
'
,
bindAnotherBeforeUnbind
:
'
请先绑定其他登录方式,再解除当前绑定。
'
,
},
source
:
{
source
:
{
avatar
:
'
头像当前来自 {providerName}
'
,
avatar
:
'
头像当前来自 {providerName}
'
,
username
:
'
昵称当前来自 {providerName}
'
,
username
:
'
昵称当前来自 {providerName}
'
,
...
...
frontend/src/types/index.ts
View file @
55513493
...
@@ -51,6 +51,7 @@ export interface UserAuthBindingStatus {
...
@@ -51,6 +51,7 @@ export interface UserAuthBindingStatus {
bind_start_path
?:
string
|
null
bind_start_path
?:
string
|
null
can_bind
?:
boolean
can_bind
?:
boolean
can_unbind
?:
boolean
can_unbind
?:
boolean
note_key
?:
string
|
null
note
?:
string
|
null
note
?:
string
|
null
metadata
?:
Record
<
string
,
unknown
>
metadata
?:
Record
<
string
,
unknown
>
}
}
...
...
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