diff --git a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts index 3008fc565..13797b9ea 100644 --- a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts +++ b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.test.ts @@ -125,7 +125,7 @@ describe('validateMandatoryUserProfile', () => { ).resolves.not.toThrow(); }); - it('identifier includes social with verified email but email occupied should throw', async () => { + it('sign-in identifier includes social with verified email but email occupied should throw', async () => { hasUserWithEmail.mockResolvedValueOnce(true); await expect( @@ -144,6 +144,26 @@ describe('validateMandatoryUserProfile', () => { ); }); + it('register identifier includes social with verified email but email occupied should throw', async () => { + hasUserWithEmail.mockResolvedValueOnce(true); + + await expect( + validateMandatoryUserProfile(users, emailRequiredCtx, { + ...interaction, + event: InteractionEvent.Register, + identifiers: [ + ...interaction.identifiers, + { key: 'social', userInfo: { email: 'email', id: 'foo' }, connectorId: 'logto' }, + ], + }) + ).rejects.toMatchError( + new RequestError( + { code: 'user.missing_profile', status: 422 }, + { missingProfile: [MissingProfile.email] } + ) + ); + }); + it('identifier includes social with verified email should not throw', async () => { hasUserWithEmail.mockResolvedValueOnce(false); @@ -200,7 +220,7 @@ describe('validateMandatoryUserProfile', () => { ).resolves.not.toThrow(); }); - it('identifier includes social with verified phone but phone occupied should throw', async () => { + it('sign-in identifier includes social with verified phone but phone occupied should throw', async () => { hasUserWithPhone.mockResolvedValueOnce(true); await expect( @@ -219,6 +239,26 @@ describe('validateMandatoryUserProfile', () => { ); }); + it('register identifier includes social with verified phone but phone occupied should throw', async () => { + hasUserWithPhone.mockResolvedValueOnce(true); + + await expect( + validateMandatoryUserProfile(users, phoneRequiredCtx, { + ...interaction, + event: InteractionEvent.Register, + identifiers: [ + ...interaction.identifiers, + { key: 'social', userInfo: { phone: '123456', id: 'foo' }, connectorId: 'logto' }, + ], + }) + ).rejects.toMatchError( + new RequestError( + { code: 'user.missing_profile', status: 422 }, + { missingProfile: [MissingProfile.phone] } + ) + ); + }); + it('identifier includes social with verified phone should not throw', async () => { hasUserWithPhone.mockResolvedValueOnce(false); diff --git a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts index 354851093..ed24c439f 100644 --- a/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts +++ b/packages/core/src/routes/interaction/verifications/mandatory-user-profile-validation.ts @@ -1,6 +1,7 @@ import type { Profile, SignInExperience, User } from '@logto/schemas'; import { InteractionEvent, MissingProfile, SignInIdentifier } from '@logto/schemas'; import type { Nullable } from '@silverhand/essentials'; +import { conditional } from '@silverhand/essentials'; import type { Context } from 'koa'; import RequestError from '#src/errors/RequestError/index.js'; @@ -119,7 +120,7 @@ const fillMissingProfileWithSocialIdentity = async ( interaction: MandatoryProfileValidationInteraction, userQueries: Queries['users'] ): Promise => { - const { identifiers, profile } = interaction; + const { identifiers, profile, event } = interaction; const socialUserInfo = getSocialUserInfo(identifiers); @@ -144,7 +145,10 @@ const fillMissingProfileWithSocialIdentity = async ( { code: 'user.missing_profile', status: 422 }, { missingProfile: Array.from(missingProfileSet), - registeredSocialIdentity: { email }, + // Throw taken email when it's sign-in event + ...conditional( + event === InteractionEvent.SignIn && { registeredSocialIdentity: { email } } + ), } ) ); @@ -178,7 +182,10 @@ const fillMissingProfileWithSocialIdentity = async ( { code: 'user.missing_profile', status: 422 }, { missingProfile: Array.from(missingProfileSet), - registeredSocialIdentity: { phone }, + // Throw taken phone when it's sign-in event + ...conditional( + event === InteractionEvent.SignIn && { registeredSocialIdentity: { phone } } + ), } ) ); diff --git a/packages/ui/src/hooks/use-required-profile-error-handler.ts b/packages/ui/src/hooks/use-required-profile-error-handler.ts index f7617720d..38f5e4a8f 100644 --- a/packages/ui/src/hooks/use-required-profile-error-handler.ts +++ b/packages/ui/src/hooks/use-required-profile-error-handler.ts @@ -27,7 +27,7 @@ const useRequiredProfileErrorHandler = ({ replace, linkSocial }: Options = {}) = // Required as a sign up method but missing in the user profile const missingProfile = data?.missingProfile[0]; - // Required as a sign up method can be found in Social Identity (email / phone), but registered with a different account + // Required as a sign up method, verified email or phone can be found in Social Identity, but registered with a different account const registeredSocialIdentity = data?.registeredSocialIdentity; const linkSocialQueryString = linkSocial