From af5c6c7f8e02aaa74dbfe063ce41f29f6beab26b Mon Sep 17 00:00:00 2001 From: wangsijie Date: Fri, 25 Oct 2024 14:50:55 +0800 Subject: [PATCH] refactor(core): use template type for code verification (#6687) --- .../connector-logto-email/src/index.ts | 2 + .../classes/experience-interaction.test.ts | 3 +- .../sign-in-experience-validator.test.ts | 5 ++- .../verifications/code-verification.ts | 39 ++++++++----------- .../verification-routes/verification-code.ts | 7 +++- .../core/src/routes/verification/index.ts | 14 +++---- .../src/__mocks__/connectors-mock.ts | 20 ++++++++++ .../connector-kit/src/types/passwordless.ts | 6 +++ 8 files changed, 60 insertions(+), 36 deletions(-) diff --git a/packages/connectors/connector-logto-email/src/index.ts b/packages/connectors/connector-logto-email/src/index.ts index 2e64e7f23..2af5da1ff 100644 --- a/packages/connectors/connector-logto-email/src/index.ts +++ b/packages/connectors/connector-logto-email/src/index.ts @@ -40,6 +40,8 @@ const sendMessage = body: { data: { to, + // TODO @wangsijie: fix this circular dependency, the connector-kit type change should be released first + // @ts-expect-error circular dependency type, payload: { ...payload, diff --git a/packages/core/src/routes/experience/classes/experience-interaction.test.ts b/packages/core/src/routes/experience/classes/experience-interaction.test.ts index 64d442385..27293dde7 100644 --- a/packages/core/src/routes/experience/classes/experience-interaction.test.ts +++ b/packages/core/src/routes/experience/classes/experience-interaction.test.ts @@ -1,3 +1,4 @@ +import { TemplateType } from '@logto/connector-kit'; import { adminConsoleApplicationId, adminTenantId, @@ -88,7 +89,7 @@ describe('ExperienceInteraction class', () => { type: SignInIdentifier.Email, value: mockEmail, }, - interactionEvent: InteractionEvent.Register, + templateType: TemplateType.Register, verified: true, }); diff --git a/packages/core/src/routes/experience/classes/libraries/sign-in-experience-validator.test.ts b/packages/core/src/routes/experience/classes/libraries/sign-in-experience-validator.test.ts index c5e216ead..afc38425b 100644 --- a/packages/core/src/routes/experience/classes/libraries/sign-in-experience-validator.test.ts +++ b/packages/core/src/routes/experience/classes/libraries/sign-in-experience-validator.test.ts @@ -1,3 +1,4 @@ +import { TemplateType } from '@logto/connector-kit'; import { InteractionEvent, type SignInExperience, @@ -68,7 +69,7 @@ const verificationCodeVerificationRecords = Object.freeze({ type: SignInIdentifier.Email, value: `foo@${emailDomain}`, }, - InteractionEvent.SignIn + TemplateType.SignIn ), [SignInIdentifier.Phone]: createNewCodeVerificationRecord( mockTenant.libraries, @@ -77,7 +78,7 @@ const verificationCodeVerificationRecords = Object.freeze({ type: SignInIdentifier.Phone, value: 'value', }, - InteractionEvent.SignIn + TemplateType.SignIn ), }); diff --git a/packages/core/src/routes/experience/classes/verifications/code-verification.ts b/packages/core/src/routes/experience/classes/verifications/code-verification.ts index d67d265ec..028ebfb0e 100644 --- a/packages/core/src/routes/experience/classes/verifications/code-verification.ts +++ b/packages/core/src/routes/experience/classes/verifications/code-verification.ts @@ -1,6 +1,6 @@ import { TemplateType, type ToZodObject } from '@logto/connector-kit'; import { - InteractionEvent, + type InteractionEvent, SignInIdentifier, VerificationType, type User, @@ -27,12 +27,9 @@ const eventToTemplateTypeMap: Record = { }; /** - * To make the typescript type checking work. A valid TemplateType is required. - * This is a work around to map the latest interaction event type to old TemplateType. - * - * @remark This is a temporary solution until the connector-kit is updated to use the latest interaction event types. + * Utility method to convert interaction event to template type. **/ -const getTemplateTypeByEvent = (event: InteractionEvent): TemplateType => +export const getTemplateTypeByEvent = (event: InteractionEvent): TemplateType => eventToTemplateTypeMap[event]; /** This util method convert the interaction identifier to passcode library payload format */ @@ -64,7 +61,7 @@ export type CodeVerificationRecordData; - interactionEvent: InteractionEvent; + templateType: TemplateType; verified: boolean; }; @@ -83,13 +80,9 @@ abstract class CodeVerification public readonly identifier: VerificationCodeIdentifierOf; /** - * The interaction event that triggered the verification. - * This will be used to determine the template type for the verification code. - * - * @remark - * `InteractionEvent.ForgotPassword` triggered verification results can not used as a verification record for other events. + * The template type for sending the verification code, the connector will use this to get the correct template. */ - public readonly interactionEvent: InteractionEvent; + public readonly templateType: TemplateType; public abstract readonly type: T; protected verified: boolean; @@ -98,11 +91,11 @@ abstract class CodeVerification private readonly queries: Queries, data: CodeVerificationRecordData ) { - const { id, identifier, verified, interactionEvent } = data; + const { id, identifier, verified, templateType } = data; this.id = id; this.identifier = identifier; - this.interactionEvent = interactionEvent; + this.templateType = templateType; this.verified = verified; } @@ -123,7 +116,7 @@ abstract class CodeVerification const verificationCode = await createPasscode( this.id, - getTemplateTypeByEvent(this.interactionEvent), + this.templateType, getPasscodeIdentifierPayload(this.identifier) ); @@ -148,7 +141,7 @@ abstract class CodeVerification await verifyPasscode( this.id, - getTemplateTypeByEvent(this.interactionEvent), + this.templateType, code, getPasscodeIdentifierPayload(identifier) ); @@ -178,13 +171,13 @@ abstract class CodeVerification } toJson(): CodeVerificationRecordData { - const { id, type, identifier, interactionEvent, verified } = this; + const { id, type, identifier, templateType, verified } = this; return { id, type, identifier, - interactionEvent, + templateType, verified, }; } @@ -194,7 +187,7 @@ abstract class CodeVerification const basicCodeVerificationRecordDataGuard = z.object({ id: z.string(), - interactionEvent: z.nativeEnum(InteractionEvent), + templateType: z.nativeEnum(TemplateType), verified: z.boolean(), }); @@ -273,7 +266,7 @@ export const createNewCodeVerificationRecord = ( identifier: | VerificationCodeIdentifier | VerificationCodeIdentifier, - interactionEvent: InteractionEvent + templateType: TemplateType ) => { const { type } = identifier; @@ -283,7 +276,7 @@ export const createNewCodeVerificationRecord = ( id: generateStandardId(), type: VerificationType.EmailVerificationCode, identifier, - interactionEvent, + templateType, verified: false, }); } @@ -292,7 +285,7 @@ export const createNewCodeVerificationRecord = ( id: generateStandardId(), type: VerificationType.PhoneVerificationCode, identifier, - interactionEvent, + templateType, verified: false, }); } diff --git a/packages/core/src/routes/experience/verification-routes/verification-code.ts b/packages/core/src/routes/experience/verification-routes/verification-code.ts index 40a15d7c6..4fee47cf3 100644 --- a/packages/core/src/routes/experience/verification-routes/verification-code.ts +++ b/packages/core/src/routes/experience/verification-routes/verification-code.ts @@ -15,7 +15,10 @@ import type TenantContext from '#src/tenants/TenantContext.js'; import type ExperienceInteraction from '../classes/experience-interaction.js'; import { withSentinel } from '../classes/libraries/sentinel-guard.js'; import { codeVerificationIdentifierRecordTypeMap } from '../classes/utils.js'; -import { createNewCodeVerificationRecord } from '../classes/verifications/code-verification.js'; +import { + createNewCodeVerificationRecord, + getTemplateTypeByEvent, +} from '../classes/verifications/code-verification.js'; import { experienceRoutes } from '../const.js'; import { type ExperienceInteractionRouterContext } from '../types.js'; @@ -68,7 +71,7 @@ export default function verificationCodeRoutes( const { identifier } = ctx.guard.body; const user = await queries.users.findUserById(userId); + const isNewIdentifier = + (identifier.type === SignInIdentifier.Email && identifier.value === user.primaryEmail) || + (identifier.type === SignInIdentifier.Phone && identifier.value === user.primaryPhone); const codeVerification = createNewCodeVerificationRecord( libraries, queries, identifier, - // TODO(LOG-10148): Add new event - InteractionEvent.SignIn + isNewIdentifier ? TemplateType.BindNewIdentifier : TemplateType.UserPermissionValidation ); await codeVerification.sendVerificationCode(); @@ -98,11 +100,7 @@ export default function verificationRoutes( const { expiresAt } = await insertVerificationRecord( codeVerification, queries, - // If the identifier is the primary email or phone, the verification record is associated with the user. - (identifier.type === SignInIdentifier.Email && identifier.value === user.primaryEmail) || - (identifier.type === SignInIdentifier.Phone && identifier.value === user.primaryPhone) - ? userId - : undefined + isNewIdentifier ? userId : undefined ); ctx.body = { diff --git a/packages/integration-tests/src/__mocks__/connectors-mock.ts b/packages/integration-tests/src/__mocks__/connectors-mock.ts index 02222ff19..06b2803df 100644 --- a/packages/integration-tests/src/__mocks__/connectors-mock.ts +++ b/packages/integration-tests/src/__mocks__/connectors-mock.ts @@ -118,6 +118,14 @@ export const mockSmsConnectorConfig = { usageType: 'Test', content: 'This is for testing purposes only. Your passcode is {{code}}.', }, + { + usageType: 'UserPermissionValidation', + content: 'This is for user permission validation purposes only. Your passcode is {{code}}.', + }, + { + usageType: 'BindNewIdentifier', + content: 'This is for binding new identifier purposes only. Your passcode is {{code}}.', + }, ], }; @@ -163,6 +171,18 @@ export const mockEmailConnectorConfig = { subject: 'Logto Organization Invitation Template', content: 'This is for organization invitation purposes only. Your link is {{link}}.', }, + { + usageType: 'UserPermissionValidation', + type: 'text/plain', + subject: 'Logto User Permission Validation Template', + content: 'This is for user permission validation purposes only. Your passcode is {{code}}.', + }, + { + usageType: 'BindNewIdentifier', + type: 'text/plain', + subject: 'Logto Bind New Identifier Template', + content: 'This is for binding new identifier purposes only. Your passcode is {{code}}.', + }, ], }; diff --git a/packages/toolkit/connector-kit/src/types/passwordless.ts b/packages/toolkit/connector-kit/src/types/passwordless.ts index 5decfc0ad..2595072e8 100644 --- a/packages/toolkit/connector-kit/src/types/passwordless.ts +++ b/packages/toolkit/connector-kit/src/types/passwordless.ts @@ -10,6 +10,8 @@ export enum VerificationCodeType { Register = 'Register', ForgotPassword = 'ForgotPassword', Generic = 'Generic', + UserPermissionValidation = 'UserPermissionValidation', + BindNewIdentifier = 'BindNewIdentifier', /** @deprecated Use `Generic` type template for sending test sms/email use case */ Test = 'Test', } @@ -28,6 +30,10 @@ export enum TemplateType { OrganizationInvitation = 'OrganizationInvitation', /** The template for generic usage. */ Generic = 'Generic', + /** The template for validating user permission for sensitive operations. */ + UserPermissionValidation = 'UserPermissionValidation', + /** The template for binding a new identifier to an existing account. */ + BindNewIdentifier = 'BindNewIdentifier', } export const templateTypeGuard = z.nativeEnum(TemplateType);