fix: clean up profile auth binding notes
This commit is contained in:
@ -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)
|
||||||
|
|||||||
@ -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) {
|
|||||||
"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) {
|
|||||||
"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": {
|
||||||
|
|||||||
@ -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 {
|
|||||||
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
|
|||||||
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,
|
|||||||
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
|
||||||
|
|||||||
@ -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(() =>
|
|||||||
? 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 {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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', () => {
|
|||||||
|
|
||||||
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', () => {
|
|||||||
|
|
||||||
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', () => {
|
|||||||
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', () => {
|
|||||||
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: {
|
||||||
|
|||||||
@ -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}',
|
||||||
|
|||||||
@ -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}',
|
||||||
|
|||||||
@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user