fix(profile): stabilize binding compatibility and frontend checks
This commit is contained in:
@@ -299,20 +299,42 @@ const emailSubmitActionLabel = computed(() =>
|
||||
: t('profile.authBindings.confirmEmailBindAction')
|
||||
)
|
||||
|
||||
function resolveLegacyCompatibleWeChatSettings(
|
||||
settings: WeChatOAuthPublicSettings | null | undefined
|
||||
): (WeChatOAuthPublicSettings & {
|
||||
wechat_oauth_open_enabled: boolean
|
||||
wechat_oauth_mp_enabled: boolean
|
||||
}) | null {
|
||||
if (!settings) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (hasExplicitWeChatOAuthCapabilities(settings)) {
|
||||
return settings
|
||||
}
|
||||
|
||||
if (typeof settings.wechat_oauth_enabled !== 'boolean') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...settings,
|
||||
wechat_oauth_open_enabled: settings.wechat_oauth_enabled,
|
||||
wechat_oauth_mp_enabled: settings.wechat_oauth_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
const wechatOAuthSettings = computed<WeChatOAuthPublicSettings | null>(() => {
|
||||
if (hasExplicitWeChatOAuthCapabilities(appStore.cachedPublicSettings)) {
|
||||
return appStore.cachedPublicSettings
|
||||
const cachedSettings = resolveLegacyCompatibleWeChatSettings(appStore.cachedPublicSettings)
|
||||
if (cachedSettings) {
|
||||
return cachedSettings
|
||||
}
|
||||
|
||||
if (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
|
||||
return resolveLegacyCompatibleWeChatSettings({
|
||||
wechat_oauth_enabled: props.wechatEnabled,
|
||||
wechat_oauth_open_enabled: props.wechatOpenEnabled,
|
||||
wechat_oauth_mp_enabled: props.wechatMpEnabled,
|
||||
})
|
||||
})
|
||||
|
||||
const resolvedWeChatBinding = computed(() => resolveWeChatOAuthStartStrict(wechatOAuthSettings.value))
|
||||
@@ -362,6 +384,17 @@ function getBindingDetails(provider: UserAuthProvider): UserAuthBindingStatus |
|
||||
return binding
|
||||
}
|
||||
|
||||
function getDisplayableEmail(user: User | null | undefined): string {
|
||||
const email = user?.email?.trim() || ''
|
||||
if (!email) {
|
||||
return ''
|
||||
}
|
||||
if (email.endsWith('.invalid') && !getBindingStatusForUser(user, 'email')) {
|
||||
return ''
|
||||
}
|
||||
return email
|
||||
}
|
||||
|
||||
function isProviderEnabledForBinding(provider: BindableProvider): boolean {
|
||||
if (provider === 'linuxdo') {
|
||||
return props.linuxdoEnabled
|
||||
@@ -444,14 +477,7 @@ function providerIconClass(provider: UserAuthProvider): string {
|
||||
|
||||
function providerSummary(provider: UserAuthProvider): string {
|
||||
if (provider === 'email') {
|
||||
const email = currentUser.value?.email?.trim() || ''
|
||||
if (!email) {
|
||||
return ''
|
||||
}
|
||||
if (currentUser.value?.email_bound === false && email.endsWith('.invalid')) {
|
||||
return ''
|
||||
}
|
||||
return email
|
||||
return getDisplayableEmail(currentUser.value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
import ProfileAvatarCard from '@/components/user/profile/ProfileAvatarCard.vue'
|
||||
import ProfileEditForm from '@/components/user/profile/ProfileEditForm.vue'
|
||||
import ProfileIdentityBindingsSection from '@/components/user/profile/ProfileIdentityBindingsSection.vue'
|
||||
import type { User, UserAuthProvider, UserProfileSourceContext } from '@/types'
|
||||
import type { User, UserAuthBindingStatus, UserAuthProvider, UserProfileSourceContext } from '@/types'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: User | null
|
||||
@@ -206,6 +206,29 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function normalizeBindingStatus(binding: boolean | UserAuthBindingStatus | undefined): boolean | null {
|
||||
if (typeof binding === 'boolean') {
|
||||
return binding
|
||||
}
|
||||
if (!binding) {
|
||||
return null
|
||||
}
|
||||
if (typeof binding.bound === 'boolean') {
|
||||
return binding.bound
|
||||
}
|
||||
return Boolean(binding.provider_subject || binding.issuer || binding.provider_key)
|
||||
}
|
||||
|
||||
function isEmailBound(user: User | null | undefined): boolean {
|
||||
if (typeof user?.email_bound === 'boolean') {
|
||||
return user.email_bound
|
||||
}
|
||||
|
||||
const nested = user?.auth_bindings?.email ?? user?.identity_bindings?.email
|
||||
const normalized = normalizeBindingStatus(nested)
|
||||
return normalized ?? false
|
||||
}
|
||||
|
||||
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(() => {
|
||||
@@ -213,7 +236,7 @@ const primaryEmailDisplay = computed(() => {
|
||||
if (!email) {
|
||||
return ''
|
||||
}
|
||||
if (props.user?.email_bound === false && email.endsWith('.invalid')) {
|
||||
if (email.endsWith('.invalid') && !isEmailBound(props.user)) {
|
||||
return ''
|
||||
}
|
||||
return email
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('ProfileIdentityBindingsSection', () => {
|
||||
expect(wrapper.find('[data-testid="profile-binding-wechat-action"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the WeChat bind action when only the legacy aggregate setting is present', () => {
|
||||
it('keeps the WeChat bind action visible when only the legacy aggregate setting is present', () => {
|
||||
const wrapper = mount(ProfileIdentityBindingsSection, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
@@ -201,7 +201,28 @@ describe('ProfileIdentityBindingsSection', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="profile-binding-wechat-action"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="profile-binding-wechat-action"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('starts the WeChat bind flow when only the legacy aggregate setting is present', async () => {
|
||||
const wrapper = mount(ProfileIdentityBindingsSection, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
props: {
|
||||
user: createUser(),
|
||||
linuxdoEnabled: false,
|
||||
oidcEnabled: false,
|
||||
wechatEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
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('mode=open')
|
||||
expect(locationState.current.href).toContain('intent=bind_current_user')
|
||||
expect(locationState.current.href).toContain('redirect=%2Fprofile')
|
||||
})
|
||||
|
||||
it('uses explicit cached WeChat capabilities and ignores legacy prop fallbacks', () => {
|
||||
@@ -358,6 +379,28 @@ describe('ProfileIdentityBindingsSection', () => {
|
||||
expect(wrapper.get('[data-testid="profile-binding-email-status"]').text()).toBe('Not bound')
|
||||
})
|
||||
|
||||
it('does not show a synthetic oauth-only email when only fallback auth bindings mark email as unbound', () => {
|
||||
const wrapper = mount(ProfileIdentityBindingsSection, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
props: {
|
||||
user: createUser({
|
||||
email: 'legacy-user@wechat-connect.invalid',
|
||||
auth_bindings: {
|
||||
email: { bound: false },
|
||||
},
|
||||
}),
|
||||
linuxdoEnabled: false,
|
||||
oidcEnabled: false,
|
||||
wechatEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).not.toContain('legacy-user@wechat-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(
|
||||
|
||||
@@ -152,6 +152,26 @@ describe('ProfileInfoCard', () => {
|
||||
expect(wrapper.text()).not.toContain('legacy-user@oidc-connect.invalid')
|
||||
})
|
||||
|
||||
it('does not display synthetic oauth-only emails when only legacy identity bindings mark email as unbound', () => {
|
||||
const wrapper = mount(ProfileInfoCard, {
|
||||
props: {
|
||||
user: createUser({
|
||||
email: 'legacy-user@wechat-connect.invalid',
|
||||
identity_bindings: {
|
||||
email: { bound: false }
|
||||
}
|
||||
})
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Icon: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.text()).not.toContain('legacy-user@wechat-connect.invalid')
|
||||
})
|
||||
|
||||
it('renders the approved overview hero and two-column content shell', () => {
|
||||
const wrapper = mount(ProfileInfoCard, {
|
||||
props: {
|
||||
|
||||
Reference in New Issue
Block a user