mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
refactor(core): sync social email and phone for new registered user
sync social verified email and phone for new registered user
This commit is contained in:
parent
baef4a7719
commit
adbed8e47d
3 changed files with 144 additions and 25 deletions
|
@ -73,7 +73,13 @@ describe('submit action', () => {
|
|||
connectorId: 'logto',
|
||||
};
|
||||
|
||||
const userInfo = { id: 'foo', name: 'foo_social', avatar: 'avatar' };
|
||||
const userInfo = {
|
||||
id: 'foo',
|
||||
name: 'foo_social',
|
||||
avatar: 'avatar',
|
||||
email: 'email@socail.com',
|
||||
phone: '123123',
|
||||
};
|
||||
|
||||
const identifiers: Identifier[] = [
|
||||
{
|
||||
|
@ -127,6 +133,37 @@ describe('submit action', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('register new social user', async () => {
|
||||
const interaction: VerifiedRegisterInteractionResult = {
|
||||
event: InteractionEvent.Register,
|
||||
profile: { connectorId: 'logto', username: 'username' },
|
||||
identifiers,
|
||||
};
|
||||
|
||||
await submitInteraction(interaction, ctx, tenant);
|
||||
|
||||
expect(generateUserId).toBeCalled();
|
||||
expect(hasActiveUsers).not.toBeCalled();
|
||||
expect(encryptUserPassword).not.toBeCalled();
|
||||
expect(getLogtoConnectorById).toBeCalledWith('logto');
|
||||
|
||||
expect(insertUser).toBeCalledWith(
|
||||
{
|
||||
id: 'uid',
|
||||
username: 'username',
|
||||
identities: {
|
||||
logto: { userId: userInfo.id, details: userInfo },
|
||||
},
|
||||
name: userInfo.name,
|
||||
avatar: userInfo.avatar,
|
||||
primaryEmail: userInfo.email,
|
||||
primaryPhone: userInfo.phone,
|
||||
lastSignInAt: now,
|
||||
},
|
||||
['user']
|
||||
);
|
||||
});
|
||||
|
||||
it('admin user register', async () => {
|
||||
hasActiveUsers.mockResolvedValueOnce(false);
|
||||
const adminConsoleCtx = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event';
|
||||
import { appInsights } from '@logto/app-insights/node';
|
||||
import type { User, Profile } from '@logto/schemas';
|
||||
import type { User, Profile, CreateUser } from '@logto/schemas';
|
||||
import {
|
||||
AdminTenantRole,
|
||||
SignInMode,
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
InteractionEvent,
|
||||
adminConsoleApplicationId,
|
||||
} from '@logto/schemas';
|
||||
import { type OmitAutoSetFields } from '@logto/shared';
|
||||
import { conditional, conditionalArray } from '@silverhand/essentials';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
|
@ -46,7 +47,6 @@ const getNewSocialProfile = async (
|
|||
}
|
||||
) => {
|
||||
// TODO: @simeng refactor me. This step should be verified by the previous profile verification cycle Already.
|
||||
// Should pickup the verified social user info result automatically
|
||||
const socialIdentifier = identifiers.find((identifier) => identifier.connectorId === connectorId);
|
||||
|
||||
if (!socialIdentifier) {
|
||||
|
@ -59,26 +59,43 @@ const getNewSocialProfile = async (
|
|||
} = await getLogtoConnectorById(connectorId);
|
||||
|
||||
const { userInfo } = socialIdentifier;
|
||||
const { name, avatar, id } = userInfo;
|
||||
const { name, avatar, id, email, phone } = userInfo;
|
||||
|
||||
// Update the user name and avatar if the connector has syncProfile enabled or is new registered user
|
||||
const profileUpdate = conditional(
|
||||
(syncProfile || !user) && {
|
||||
const identities = { ...user?.identities, [target]: { userId: id, details: userInfo } };
|
||||
|
||||
// Sync the name, avatar, email and phone for new user
|
||||
if (!user) {
|
||||
return {
|
||||
identities,
|
||||
...conditional(name && { name }),
|
||||
...conditional(avatar && { avatar }),
|
||||
}
|
||||
);
|
||||
...conditional(email && { primaryEmail: email }),
|
||||
...conditional(phone && { primaryPhone: phone }),
|
||||
};
|
||||
}
|
||||
|
||||
// Sync the user name and avatar if the connector has syncProfile enabled
|
||||
return {
|
||||
identities: { ...user?.identities, [target]: { userId: id, details: userInfo } },
|
||||
...profileUpdate,
|
||||
identities,
|
||||
...conditional(
|
||||
syncProfile && {
|
||||
...conditional(name && { name }),
|
||||
...conditional(avatar && { avatar }),
|
||||
}
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const getSyncedSocialUserProfile = async (
|
||||
const getLatestUserProfileFromSocial = async (
|
||||
{ getLogtoConnectorById }: ConnectorLibrary,
|
||||
socialIdentifier: SocialIdentifier
|
||||
authIdentifiers: Identifier[]
|
||||
) => {
|
||||
const socialIdentifier = filterSocialIdentifiers(authIdentifiers).slice(-1)[0];
|
||||
|
||||
if (!socialIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
userInfo: { name, avatar },
|
||||
connectorId,
|
||||
|
@ -117,11 +134,11 @@ const parseNewUserProfile = async (
|
|||
]);
|
||||
|
||||
return {
|
||||
...socialProfile, // SocialProfile should be applied first
|
||||
...passwordProfile,
|
||||
...conditional(phone && { primaryPhone: phone }),
|
||||
...conditional(username && { username }),
|
||||
...conditional(email && { primaryEmail: email }),
|
||||
...passwordProfile,
|
||||
...socialProfile,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -129,17 +146,15 @@ const parseUserProfile = async (
|
|||
connectorLibrary: ConnectorLibrary,
|
||||
{ profile, identifiers }: VerifiedSignInInteractionResult | VerifiedRegisterInteractionResult,
|
||||
user?: User
|
||||
) => {
|
||||
): Promise<Omit<OmitAutoSetFields<CreateUser>, 'id'>> => {
|
||||
const { authIdentifiers, profileIdentifiers } = categorizeIdentifiers(identifiers ?? [], profile);
|
||||
|
||||
const newUserProfile =
|
||||
profile && (await parseNewUserProfile(connectorLibrary, profile, profileIdentifiers, user));
|
||||
|
||||
// Sync the last social profile
|
||||
const socialIdentifier = filterSocialIdentifiers(authIdentifiers).slice(-1)[0];
|
||||
|
||||
// Sync from the latest social identity profile for existing users
|
||||
const syncedSocialUserProfile =
|
||||
socialIdentifier && (await getSyncedSocialUserProfile(connectorLibrary, socialIdentifier));
|
||||
user && (await getLatestUserProfileFromSocial(connectorLibrary, authIdentifiers));
|
||||
|
||||
return {
|
||||
...syncedSocialUserProfile,
|
||||
|
@ -151,7 +166,7 @@ const parseUserProfile = async (
|
|||
export default async function submitInteraction(
|
||||
interaction: VerifiedInteractionResult,
|
||||
ctx: WithInteractionDetailsContext,
|
||||
{ provider, libraries, connectors, queries, id: tenantId }: TenantContext,
|
||||
{ provider, libraries, connectors, queries }: TenantContext,
|
||||
log?: LogEntry
|
||||
) {
|
||||
const { hasActiveUsers, findUserById, updateUserById } = queries.users;
|
||||
|
@ -164,7 +179,7 @@ export default async function submitInteraction(
|
|||
|
||||
if (event === InteractionEvent.Register) {
|
||||
const id = await generateUserId();
|
||||
const upsertProfile = await parseUserProfile(connectors, interaction);
|
||||
const userProfile = await parseUserProfile(connectors, interaction);
|
||||
|
||||
const { client_id } = ctx.interactionDetails.params;
|
||||
|
||||
|
@ -178,7 +193,7 @@ export default async function submitInteraction(
|
|||
await insertUser(
|
||||
{
|
||||
id,
|
||||
...upsertProfile,
|
||||
...userProfile,
|
||||
},
|
||||
conditionalArray<string>(
|
||||
isInAdminTenant && AdminTenantRole.User,
|
||||
|
@ -208,9 +223,9 @@ export default async function submitInteraction(
|
|||
|
||||
if (event === InteractionEvent.SignIn) {
|
||||
const user = await findUserById(accountId);
|
||||
const upsertProfile = await parseUserProfile(connectors, interaction, user);
|
||||
const updateUserProfile = await parseUserProfile(connectors, interaction, user);
|
||||
|
||||
await updateUserById(accountId, upsertProfile);
|
||||
await updateUserById(accountId, updateUserProfile);
|
||||
await assignInteractionResults(ctx, provider, { login: { accountId } });
|
||||
|
||||
appInsights.client?.trackEvent({ name: getEventName(Component.Core, CoreEvent.SignIn) });
|
||||
|
|
|
@ -4,6 +4,7 @@ import { mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js';
|
|||
import {
|
||||
createSocialAuthorizationUri,
|
||||
putInteraction,
|
||||
getUser,
|
||||
deleteUser,
|
||||
putInteractionEvent,
|
||||
patchInteractionIdentifiers,
|
||||
|
@ -97,6 +98,72 @@ describe('Social Identifier Interactions', () => {
|
|||
await logoutClient(client);
|
||||
await deleteUser(id);
|
||||
});
|
||||
|
||||
it('register with social and synced email', async () => {
|
||||
const client = await initClient();
|
||||
const socialEmail = generateEmail();
|
||||
|
||||
const connectorId = connectorIdMap.get(mockSocialConnectorId) ?? '';
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await client.successSend(createSocialAuthorizationUri, { state, redirectUri, connectorId });
|
||||
|
||||
await client.successSend(patchInteractionIdentifiers, {
|
||||
connectorId,
|
||||
connectorData: { state, redirectUri, code, userId: socialUserId, email: socialEmail },
|
||||
});
|
||||
|
||||
await expectRejects(client.submitInteraction(), 'user.identity_not_exist');
|
||||
|
||||
await client.successSend(putInteractionEvent, { event: InteractionEvent.Register });
|
||||
await client.successSend(putInteractionProfile, { connectorId });
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
|
||||
const uid = await processSession(client, redirectTo);
|
||||
|
||||
const { primaryEmail } = await getUser(uid);
|
||||
expect(primaryEmail).toBe(socialEmail);
|
||||
|
||||
await logoutClient(client);
|
||||
await deleteUser(uid);
|
||||
});
|
||||
|
||||
it('register with social and synced phone', async () => {
|
||||
const client = await initClient();
|
||||
const socialPhone = generatePhone();
|
||||
|
||||
const connectorId = connectorIdMap.get(mockSocialConnectorId) ?? '';
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await client.successSend(createSocialAuthorizationUri, { state, redirectUri, connectorId });
|
||||
|
||||
await client.successSend(patchInteractionIdentifiers, {
|
||||
connectorId,
|
||||
connectorData: { state, redirectUri, code, userId: socialUserId, phone: socialPhone },
|
||||
});
|
||||
|
||||
await expectRejects(client.submitInteraction(), 'user.identity_not_exist');
|
||||
|
||||
await client.successSend(putInteractionEvent, { event: InteractionEvent.Register });
|
||||
await client.successSend(putInteractionProfile, { connectorId });
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
|
||||
const uid = await processSession(client, redirectTo);
|
||||
|
||||
const { primaryPhone } = await getUser(uid);
|
||||
expect(primaryPhone).toBe(socialPhone);
|
||||
|
||||
await logoutClient(client);
|
||||
await deleteUser(uid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bind with existing email account', () => {
|
||||
|
|
Loading…
Add table
Reference in a new issue