Skip to content

Commit

Permalink
refactor(core): extract helpers and provision methods
Browse files Browse the repository at this point in the history
extract helpers and provision methods
  • Loading branch information
simeng-li committed Jul 22, 2024
1 parent 39e834b commit 09161a2
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 179 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type ToZodObject } from '@logto/connector-kit';
import { InteractionEvent, type User, type VerificationType } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { conditional } from '@silverhand/essentials';
import { z } from 'zod';

Expand All @@ -11,14 +10,14 @@ import assertThat from '#src/utils/assert-that.js';

import { interactionProfileGuard, type Interaction, type InteractionProfile } from '../types.js';

import { ProvisionLibrary } from './provision-library.js';
import {
getNewUserProfileFromVerificationRecord,
identifyUserByVerificationRecord,
toUserSocialIdentityData,
} from './utils.js';
} from './helpers.js';
import { toUserSocialIdentityData } from './utils.js';
import { MfaValidator } from './validators/mfa-validator.js';
import { ProfileValidator } from './validators/profile-validator.js';
import { ProvisionLibrary } from './validators/provision-library.js';
import { SignInExperienceValidator } from './validators/sign-in-experience-validator.js';
import {
buildVerificationRecord,
Expand Down Expand Up @@ -274,14 +273,7 @@ export default class ExperienceInteraction {
// TODO: missing profile fields validation

if (enterpriseSsoIdentity) {
await userSsoIdentitiesQueries.insert({
id: generateStandardId(),
userId: user.id,
...enterpriseSsoIdentity,
});
await this.provisionLibrary.newUserJtiOrganizationProvision(user.id, {
enterpriseSsoIdentity,
});
await this.provisionLibrary.provisionNewSsoIdentity(user.id, enterpriseSsoIdentity);

Check warning on line 276 in packages/core/src/routes/experience/classes/experience-interaction.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L276

Added line #L276 was not covered by tests
}

const { provider } = this.tenant;
Expand Down Expand Up @@ -356,47 +348,10 @@ export default class ExperienceInteraction {
* @throws {RequestError} with 400 if the verification record is invalid for creating a new user or not verified
*/
private async createNewUser(verificationRecord: VerificationRecord) {
const {
libraries: {
users: { generateUserId, insertUser },
},
queries: { userSsoIdentities: userSsoIdentitiesQueries },
} = this.tenant;

const newProfile = await getNewUserProfileFromVerificationRecord(verificationRecord);

await this.profileValidator.guardProfileUniquenessAcrossUsers(newProfile);

const { socialIdentity, enterpriseSsoIdentity, ...rest } = newProfile;

const { isCreatingFirstAdminUser, initialUserRoles, customData } =
await this.provisionLibrary.getUserProvisionContext(newProfile);

const [user] = await insertUser(
{
id: await generateUserId(),
...rest,
...conditional(socialIdentity && { identities: toUserSocialIdentityData(socialIdentity) }),
...conditional(customData && { customData }),
},
initialUserRoles
);

if (isCreatingFirstAdminUser) {
await this.provisionLibrary.adminUserProvision(user);
}

if (enterpriseSsoIdentity) {
await userSsoIdentitiesQueries.insert({
id: generateStandardId(),
userId: user.id,
...enterpriseSsoIdentity,
});
}

await this.provisionLibrary.newUserJtiOrganizationProvision(user.id, newProfile);

// TODO: new user hooks
const user = await this.provisionLibrary.provisionNewUser(newProfile);

this.userId = user.id;
}
Expand Down
108 changes: 108 additions & 0 deletions packages/core/src/routes/experience/classes/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @overview This file contains helper functions for the main interaction class.
*
* To help reduce the complexity and code amount of the main interaction class,
* we have moved some of the standalone functions into this file.
*/

import { VerificationType, type User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';

import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '#src/utils/assert-that.js';

import type { InteractionProfile } from '../types.js';

import { type VerificationRecord } from './verifications/index.js';

/**
* @throws {RequestError} -400 if the verification record type is not supported for user creation.
* @throws {RequestError} -400 if the verification record is not verified.
*/
export const getNewUserProfileFromVerificationRecord = async (
verificationRecord: VerificationRecord
): Promise<InteractionProfile> => {
switch (verificationRecord.type) {
case VerificationType.NewPasswordIdentity:
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
return verificationRecord.toUserProfile();
}
case VerificationType.EnterpriseSso:
case VerificationType.Social: {
const identityProfile = await verificationRecord.toUserProfile();
const syncedProfile = await verificationRecord.toSyncedProfile(true);
return { ...identityProfile, ...syncedProfile };
}

Check warning on line 36 in packages/core/src/routes/experience/classes/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/helpers.ts#L33-L36

Added lines #L33 - L36 were not covered by tests
default: {
// Unsupported verification type for user creation, such as MFA verification.
throw new RequestError({ code: 'session.verification_failed', status: 400 });
}

Check warning on line 40 in packages/core/src/routes/experience/classes/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/helpers.ts#L38-L40

Added lines #L38 - L40 were not covered by tests
}
};

/**
* @throws {RequestError} -400 if the verification record type is not supported for user identification.
* @throws {RequestError} -400 if the verification record is not verified.
* @throws {RequestError} -404 if the user is not found.
*/
export const identifyUserByVerificationRecord = async (
verificationRecord: VerificationRecord,
linkSocialIdentity?: boolean
): Promise<{
user: User;
/**
* Returns the social/enterprise SSO synced profiled if the verification record is a social/enterprise SSO verification.
* - For new linked identity, the synced profile will includes the new social or enterprise SSO identity.
* - For existing social or enterprise SSO identity, the synced profile will return the synced user profile based on connector settings.
*/
syncedProfile?: Pick<
InteractionProfile,
'enterpriseSsoIdentity' | 'socialIdentity' | 'avatar' | 'name'
>;
}> => {
// Check verification record can be used to identify a user using the `identifyUser` method.
// E.g. MFA verification record does not have the `identifyUser` method, cannot be used to identify a user.
assertThat(
'identifyUser' in verificationRecord,
new RequestError({ code: 'session.verification_failed', status: 400 })
);

switch (verificationRecord.type) {
case VerificationType.Password:
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
return { user: await verificationRecord.identifyUser() };
}
case VerificationType.Social: {
const user = linkSocialIdentity
? await verificationRecord.identifyRelatedUser()
: await verificationRecord.identifyUser();

const syncedProfile = {
...(await verificationRecord.toSyncedProfile()),
...conditional(linkSocialIdentity && (await verificationRecord.toUserProfile())),
};

return { user, syncedProfile };
}
case VerificationType.EnterpriseSso: {
try {
const user = await verificationRecord.identifyUser();
const syncedProfile = await verificationRecord.toSyncedProfile();
return { user, syncedProfile };
} catch (error: unknown) {
// Auto fallback to identify the related user if the user does not exist for enterprise SSO.
if (error instanceof RequestError && error.code === 'user.identity_not_exist') {
const user = await verificationRecord.identifyRelatedUser();
const syncedProfile = {
...(await verificationRecord.toUserProfile()),
...(await verificationRecord.toSyncedProfile()),
};
return { user, syncedProfile };
}
throw error;
}
}
}
};

Check warning on line 108 in packages/core/src/routes/experience/classes/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/helpers.ts#L50-L108

Added lines #L50 - L108 were not covered by tests
103 changes: 3 additions & 100 deletions packages/core/src/routes/experience/classes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@ import {
type User,
type VerificationCodeSignInIdentifier,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';

import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';

import type { InteractionProfile } from '../types.js';

import { type VerificationRecord } from './verifications/index.js';

export const findUserByIdentifier = async (
userQuery: Queries['users'],
{ type, value }: InteractionIdentifier
Expand All @@ -32,98 +27,6 @@ export const findUserByIdentifier = async (
}
};

/**
* @throws {RequestError} -400 if the verification record type is not supported for user creation.
* @throws {RequestError} -400 if the verification record is not verified.
*/
export const getNewUserProfileFromVerificationRecord = async (
verificationRecord: VerificationRecord
): Promise<InteractionProfile> => {
switch (verificationRecord.type) {
case VerificationType.NewPasswordIdentity:
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
return verificationRecord.toUserProfile();
}
case VerificationType.EnterpriseSso:
case VerificationType.Social: {
const identityProfile = await verificationRecord.toUserProfile();
const syncedProfile = await verificationRecord.toSyncedProfile(true);
return { ...identityProfile, ...syncedProfile };
}
default: {
// Unsupported verification type for user creation, such as MFA verification.
throw new RequestError({ code: 'session.verification_failed', status: 400 });
}
}
};

/**
* @throws {RequestError} -400 if the verification record type is not supported for user identification.
* @throws {RequestError} -400 if the verification record is not verified.
* @throws {RequestError} -404 if the user is not found.
*/
export const identifyUserByVerificationRecord = async (
verificationRecord: VerificationRecord,
linkSocialIdentity?: boolean
): Promise<{
user: User;
/**
* Returns the social/enterprise SSO synced profiled if the verification record is a social/enterprise SSO verification.
* - For new linked identity, the synced profile will includes the new social or enterprise SSO identity.
* - For existing social or enterprise SSO identity, the synced profile will return the synced user profile based on connector settings.
*/
syncedProfile?: Pick<
InteractionProfile,
'enterpriseSsoIdentity' | 'socialIdentity' | 'avatar' | 'name'
>;
}> => {
// Check verification record can be used to identify a user using the `identifyUser` method.
// E.g. MFA verification record does not have the `identifyUser` method, cannot be used to identify a user.
assertThat(
'identifyUser' in verificationRecord,
new RequestError({ code: 'session.verification_failed', status: 400 })
);

switch (verificationRecord.type) {
case VerificationType.Password:
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
return { user: await verificationRecord.identifyUser() };
}
case VerificationType.Social: {
const user = linkSocialIdentity
? await verificationRecord.identifyRelatedUser()
: await verificationRecord.identifyUser();

const syncedProfile = {
...(await verificationRecord.toSyncedProfile()),
...conditional(linkSocialIdentity && (await verificationRecord.toUserProfile())),
};

return { user, syncedProfile };
}
case VerificationType.EnterpriseSso: {
try {
const user = await verificationRecord.identifyUser();
const syncedProfile = await verificationRecord.toSyncedProfile();
return { user, syncedProfile };
} catch (error: unknown) {
// Auto fallback to identify the related user if the user does not exist for enterprise SSO.
if (error instanceof RequestError && error.code === 'user.identity_not_exist') {
const user = await verificationRecord.identifyRelatedUser();
const syncedProfile = {
...(await verificationRecord.toUserProfile()),
...(await verificationRecord.toSyncedProfile()),
};
return { user, syncedProfile };
}
throw error;
}
}
}
};

/**
* Convert the interaction profile `socialIdentity` to `User['identities']` data format
*/
Expand All @@ -140,9 +43,9 @@ export const toUserSocialIdentityData = (
};
};

export function interactionIdentifierToUserProfile(
export const interactionIdentifierToUserProfile = (
identifier: InteractionIdentifier
): { username: string } | { primaryEmail: string } | { primaryPhone: string } {
): { username: string } | { primaryEmail: string } | { primaryPhone: string } => {
const { type, value } = identifier;
switch (type) {
case SignInIdentifier.Username: {
Expand All @@ -155,7 +58,7 @@ export function interactionIdentifierToUserProfile(
return { primaryPhone: value };
}
}
}
};

export const codeVerificationIdentifierRecordTypeMap = Object.freeze({
[SignInIdentifier.Email]: VerificationType.EmailVerificationCode,
Expand Down
Loading

0 comments on commit 09161a2

Please sign in to comment.