fix(profile): stabilize identity binding management

This commit is contained in:
IanShaw027
2026-04-22 13:19:28 +08:00
parent 83cad63ce0
commit 81c827ee51
13 changed files with 584 additions and 39 deletions

View File

@@ -444,7 +444,14 @@ function providerIconClass(provider: UserAuthProvider): string {
function providerSummary(provider: UserAuthProvider): string {
if (provider === 'email') {
return currentUser.value?.email || ''
const email = currentUser.value?.email?.trim() || ''
if (!email) {
return ''
}
if (currentUser.value?.email_bound === false && email.endsWith('.invalid')) {
return ''
}
return email
}
return ''
}

View File

@@ -40,7 +40,7 @@
<div class="space-y-1">
<p class="truncate text-sm text-gray-600 dark:text-gray-300">
{{ user?.email }}
{{ primaryEmailDisplay }}
</p>
<div
v-if="sourceHints.length"
@@ -208,6 +208,16 @@ const { t } = useI18n()
const avatarUrl = computed(() => props.user?.avatar_url?.trim() || '')
const displayName = computed(() => props.user?.username?.trim() || props.user?.email?.trim() || t('profile.user'))
const primaryEmailDisplay = computed(() => {
const email = props.user?.email?.trim() || ''
if (!email) {
return ''
}
if (props.user?.email_bound === false && email.endsWith('.invalid')) {
return ''
}
return email
})
const avatarInitial = computed(() => displayName.value.charAt(0).toUpperCase() || 'U')
const memberSinceLabel = computed(() => {
const raw = props.user?.created_at?.trim()
@@ -229,7 +239,7 @@ const memberSinceLabel = computed(() => {
const providerLabels = computed<Record<UserAuthProvider, string>>(() => ({
email: t('profile.authBindings.providers.email'),
linuxdo: t('profile.authBindings.providers.linuxdo'),
oidc: t('profile.authBindings.providers.oidc', { providerName: 'OIDC' }),
oidc: t('profile.authBindings.providers.oidc', { providerName: props.oidcProviderName }),
wechat: t('profile.authBindings.providers.wechat')
}))

View File

@@ -335,6 +335,29 @@ describe('ProfileIdentityBindingsSection', () => {
expect(wrapper.get('[data-testid="profile-binding-email-input"]').exists()).toBe(true)
})
it('does not show a synthetic oauth-only email as the bound email summary', () => {
const wrapper = mount(ProfileIdentityBindingsSection, {
global: {
plugins: [pinia],
},
props: {
user: createUser({
email: 'legacy-user@linuxdo-connect.invalid',
email_bound: false,
auth_bindings: {
email: { bound: false },
},
}),
linuxdoEnabled: false,
oidcEnabled: false,
wechatEnabled: false,
},
})
expect(wrapper.text()).not.toContain('legacy-user@linuxdo-connect.invalid')
expect(wrapper.get('[data-testid="profile-binding-email-status"]').text()).toBe('Not bound')
})
it('keeps the email form available for replacing a bound primary email', async () => {
userApiMocks.sendEmailBindingCode.mockResolvedValue(undefined)
userApiMocks.bindEmailIdentity.mockResolvedValue(

View File

@@ -111,6 +111,47 @@ describe('ProfileInfoCard', () => {
expect(wrapper.text()).toContain('Username synced from LinuxDo')
})
it('uses the configured OIDC provider name in source hints', () => {
const wrapper = mount(ProfileInfoCard, {
props: {
user: createUser({
profile_sources: {
username: { provider: 'oidc', source: 'oidc' }
}
}),
oidcProviderName: 'ExampleID'
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).toContain('Username synced from ExampleID')
})
it('does not display synthetic oauth-only emails as a real bound email', () => {
const wrapper = mount(ProfileInfoCard, {
props: {
user: createUser({
email: 'legacy-user@oidc-connect.invalid',
email_bound: false,
auth_bindings: {
email: { bound: false }
}
})
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).not.toContain('legacy-user@oidc-connect.invalid')
})
it('renders the approved overview hero and two-column content shell', () => {
const wrapper = mount(ProfileInfoCard, {
props: {