mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor(core,schemas): refactor CodeVerification
(#6277)
* refactor(core,schemas): refactor the CodeVerification class split the CodeVerification class into EmailCodeVerification and PhoneCodeVerification * refactor(core,schemas): split CodeVerification type split CodeVerification type * fix(core): code review updates code review updates
This commit is contained in:
parent
8c0958ff6c
commit
de61735353
9 changed files with 204 additions and 99 deletions
packages
core/src/routes/experience
schemas/src/types
|
@ -17,7 +17,7 @@ import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||||
|
|
||||||
import { CodeVerification } from './verifications/code-verification.js';
|
import { EmailCodeVerification } from './verifications/code-verification.js';
|
||||||
|
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
const { mockEsm } = createMockUtils(jest);
|
const { mockEsm } = createMockUtils(jest);
|
||||||
|
@ -75,9 +75,9 @@ describe('ExperienceInteraction class', () => {
|
||||||
};
|
};
|
||||||
const { libraries, queries } = tenant;
|
const { libraries, queries } = tenant;
|
||||||
|
|
||||||
const emailVerificationRecord = new CodeVerification(libraries, queries, {
|
const emailVerificationRecord = new EmailCodeVerification(libraries, queries, {
|
||||||
id: 'mock_email_verification_id',
|
id: 'mock_email_verification_id',
|
||||||
type: VerificationType.VerificationCode,
|
type: VerificationType.EmailVerificationCode,
|
||||||
identifier: {
|
identifier: {
|
||||||
type: SignInIdentifier.Email,
|
type: SignInIdentifier.Email,
|
||||||
value: mockEmail,
|
value: mockEmail,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
VerificationType,
|
VerificationType,
|
||||||
type InteractionIdentifier,
|
type InteractionIdentifier,
|
||||||
type User,
|
type User,
|
||||||
|
type VerificationCodeSignInIdentifier,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
@ -41,7 +42,8 @@ export const getNewUserProfileFromVerificationRecord = async (
|
||||||
): Promise<InteractionProfile> => {
|
): Promise<InteractionProfile> => {
|
||||||
switch (verificationRecord.type) {
|
switch (verificationRecord.type) {
|
||||||
case VerificationType.NewPasswordIdentity:
|
case VerificationType.NewPasswordIdentity:
|
||||||
case VerificationType.VerificationCode: {
|
case VerificationType.EmailVerificationCode:
|
||||||
|
case VerificationType.PhoneVerificationCode: {
|
||||||
return verificationRecord.toUserProfile();
|
return verificationRecord.toUserProfile();
|
||||||
}
|
}
|
||||||
case VerificationType.EnterpriseSso:
|
case VerificationType.EnterpriseSso:
|
||||||
|
@ -86,7 +88,8 @@ export const identifyUserByVerificationRecord = async (
|
||||||
|
|
||||||
switch (verificationRecord.type) {
|
switch (verificationRecord.type) {
|
||||||
case VerificationType.Password:
|
case VerificationType.Password:
|
||||||
case VerificationType.VerificationCode: {
|
case VerificationType.EmailVerificationCode:
|
||||||
|
case VerificationType.PhoneVerificationCode: {
|
||||||
return { user: await verificationRecord.identifyUser() };
|
return { user: await verificationRecord.identifyUser() };
|
||||||
}
|
}
|
||||||
case VerificationType.Social: {
|
case VerificationType.Social: {
|
||||||
|
@ -171,3 +174,8 @@ export function profileToUserInfo(
|
||||||
phoneNumber: primaryPhone ?? undefined,
|
phoneNumber: primaryPhone ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const codeVerificationIdentifierRecordTypeMap = Object.freeze({
|
||||||
|
[SignInIdentifier.Email]: VerificationType.EmailVerificationCode,
|
||||||
|
[SignInIdentifier.Phone]: VerificationType.PhoneVerificationCode,
|
||||||
|
}) satisfies Record<VerificationCodeSignInIdentifier, VerificationType>;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||||
|
|
||||||
import { CodeVerification } from '../verifications/code-verification.js';
|
import { createNewCodeVerificationRecord } from '../verifications/code-verification.js';
|
||||||
import { EnterpriseSsoVerification } from '../verifications/enterprise-sso-verification.js';
|
import { EnterpriseSsoVerification } from '../verifications/enterprise-sso-verification.js';
|
||||||
import { type VerificationRecord } from '../verifications/index.js';
|
import { type VerificationRecord } from '../verifications/index.js';
|
||||||
import { NewPasswordIdentityVerification } from '../verifications/new-password-identity-verification.js';
|
import { NewPasswordIdentityVerification } from '../verifications/new-password-identity-verification.js';
|
||||||
|
@ -53,7 +53,7 @@ const passwordVerificationRecords = Object.fromEntries(
|
||||||
) as Record<SignInIdentifier, PasswordVerification>;
|
) as Record<SignInIdentifier, PasswordVerification>;
|
||||||
|
|
||||||
const verificationCodeVerificationRecords = Object.freeze({
|
const verificationCodeVerificationRecords = Object.freeze({
|
||||||
[SignInIdentifier.Email]: CodeVerification.create(
|
[SignInIdentifier.Email]: createNewCodeVerificationRecord(
|
||||||
mockTenant.libraries,
|
mockTenant.libraries,
|
||||||
mockTenant.queries,
|
mockTenant.queries,
|
||||||
{
|
{
|
||||||
|
@ -62,7 +62,7 @@ const verificationCodeVerificationRecords = Object.freeze({
|
||||||
},
|
},
|
||||||
InteractionEvent.SignIn
|
InteractionEvent.SignIn
|
||||||
),
|
),
|
||||||
[SignInIdentifier.Phone]: CodeVerification.create(
|
[SignInIdentifier.Phone]: createNewCodeVerificationRecord(
|
||||||
mockTenant.libraries,
|
mockTenant.libraries,
|
||||||
mockTenant.queries,
|
mockTenant.queries,
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { PasswordPolicyChecker } from '@logto/core-kit';
|
||||||
import {
|
import {
|
||||||
InteractionEvent,
|
InteractionEvent,
|
||||||
type SignInExperience,
|
type SignInExperience,
|
||||||
|
SignInIdentifier,
|
||||||
SignInMode,
|
SignInMode,
|
||||||
VerificationType,
|
VerificationType,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
@ -18,12 +19,13 @@ import { type VerificationRecord } from '../verifications/index.js';
|
||||||
const getEmailIdentifierFromVerificationRecord = (verificationRecord: VerificationRecord) => {
|
const getEmailIdentifierFromVerificationRecord = (verificationRecord: VerificationRecord) => {
|
||||||
switch (verificationRecord.type) {
|
switch (verificationRecord.type) {
|
||||||
case VerificationType.Password:
|
case VerificationType.Password:
|
||||||
case VerificationType.VerificationCode: {
|
case VerificationType.EmailVerificationCode:
|
||||||
|
case VerificationType.PhoneVerificationCode: {
|
||||||
const {
|
const {
|
||||||
identifier: { type, value },
|
identifier: { type, value },
|
||||||
} = verificationRecord;
|
} = verificationRecord;
|
||||||
|
|
||||||
return type === 'email' ? value : undefined;
|
return type === SignInIdentifier.Email ? value : undefined;
|
||||||
}
|
}
|
||||||
case VerificationType.Social: {
|
case VerificationType.Social: {
|
||||||
const { socialUserInfo } = verificationRecord;
|
const { socialUserInfo } = verificationRecord;
|
||||||
|
@ -174,7 +176,8 @@ export class SignInExperienceValidator {
|
||||||
|
|
||||||
switch (verificationRecord.type) {
|
switch (verificationRecord.type) {
|
||||||
case VerificationType.Password:
|
case VerificationType.Password:
|
||||||
case VerificationType.VerificationCode: {
|
case VerificationType.EmailVerificationCode:
|
||||||
|
case VerificationType.PhoneVerificationCode: {
|
||||||
const {
|
const {
|
||||||
identifier: { type },
|
identifier: { type },
|
||||||
} = verificationRecord;
|
} = verificationRecord;
|
||||||
|
@ -224,7 +227,8 @@ export class SignInExperienceValidator {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case VerificationType.VerificationCode: {
|
case VerificationType.EmailVerificationCode:
|
||||||
|
case VerificationType.PhoneVerificationCode: {
|
||||||
const {
|
const {
|
||||||
identifier: { type },
|
identifier: { type },
|
||||||
} = verificationRecord;
|
} = verificationRecord;
|
||||||
|
@ -255,7 +259,8 @@ export class SignInExperienceValidator {
|
||||||
/** Forgot password only supports verification code type verification record */
|
/** Forgot password only supports verification code type verification record */
|
||||||
private guardForgotPasswordVerificationMethod(verificationRecord: VerificationRecord) {
|
private guardForgotPasswordVerificationMethod(verificationRecord: VerificationRecord) {
|
||||||
assertThat(
|
assertThat(
|
||||||
verificationRecord.type === VerificationType.VerificationCode,
|
verificationRecord.type === VerificationType.EmailVerificationCode ||
|
||||||
|
verificationRecord.type === VerificationType.PhoneVerificationCode,
|
||||||
new RequestError({ code: 'session.not_supported_for_forgot_password', status: 422 })
|
new RequestError({ code: 'session.not_supported_for_forgot_password', status: 422 })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { TemplateType } from '@logto/connector-kit';
|
import { TemplateType, type ToZodObject } from '@logto/connector-kit';
|
||||||
import {
|
import {
|
||||||
InteractionEvent,
|
InteractionEvent,
|
||||||
|
SignInIdentifier,
|
||||||
VerificationType,
|
VerificationType,
|
||||||
verificationCodeIdentifierGuard,
|
|
||||||
type User,
|
type User,
|
||||||
type VerificationCodeIdentifier,
|
type VerificationCodeIdentifier,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { type ToZodObject } from '@logto/schemas/lib/utils/zod.js';
|
|
||||||
import { generateStandardId } from '@logto/shared';
|
import { generateStandardId } from '@logto/shared';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
@ -35,64 +34,47 @@ const eventToTemplateTypeMap: Record<InteractionEvent, TemplateType> = {
|
||||||
const getTemplateTypeByEvent = (event: InteractionEvent): TemplateType =>
|
const getTemplateTypeByEvent = (event: InteractionEvent): TemplateType =>
|
||||||
eventToTemplateTypeMap[event];
|
eventToTemplateTypeMap[event];
|
||||||
|
|
||||||
/** The JSON data type for the CodeVerification record */
|
|
||||||
export type CodeVerificationRecordData = {
|
|
||||||
id: string;
|
|
||||||
type: VerificationType.VerificationCode;
|
|
||||||
identifier: VerificationCodeIdentifier;
|
|
||||||
interactionEvent: InteractionEvent;
|
|
||||||
verified: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const codeVerificationRecordDataGuard = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
type: z.literal(VerificationType.VerificationCode),
|
|
||||||
identifier: verificationCodeIdentifierGuard,
|
|
||||||
interactionEvent: z.nativeEnum(InteractionEvent),
|
|
||||||
verified: z.boolean(),
|
|
||||||
}) satisfies ToZodObject<CodeVerificationRecordData>;
|
|
||||||
|
|
||||||
/** This util method convert the interaction identifier to passcode library payload format */
|
/** This util method convert the interaction identifier to passcode library payload format */
|
||||||
const getPasscodeIdentifierPayload = (
|
const getPasscodeIdentifierPayload = (
|
||||||
identifier: VerificationCodeIdentifier
|
identifier: VerificationCodeIdentifier
|
||||||
): Parameters<ReturnType<typeof createPasscodeLibrary>['createPasscode']>[2] =>
|
): Parameters<ReturnType<typeof createPasscodeLibrary>['createPasscode']>[2] =>
|
||||||
identifier.type === 'email' ? { email: identifier.value } : { phone: identifier.value };
|
identifier.type === 'email' ? { email: identifier.value } : { phone: identifier.value };
|
||||||
|
|
||||||
|
type CodeVerificationType =
|
||||||
|
| VerificationType.EmailVerificationCode
|
||||||
|
| VerificationType.PhoneVerificationCode;
|
||||||
|
|
||||||
|
type SinInIdentifierTypeOf = {
|
||||||
|
[VerificationType.EmailVerificationCode]: SignInIdentifier.Email;
|
||||||
|
[VerificationType.PhoneVerificationCode]: SignInIdentifier.Phone;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VerificationCodeIdentifierOf<T extends CodeVerificationType> = VerificationCodeIdentifier<
|
||||||
|
SinInIdentifierTypeOf[T]
|
||||||
|
>;
|
||||||
|
|
||||||
|
type CodeVerificationIdentifierMap = {
|
||||||
|
[VerificationType.EmailVerificationCode]: { primaryEmail: string };
|
||||||
|
[VerificationType.PhoneVerificationCode]: { primaryPhone: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The JSON data type for the `CodeVerification` record */
|
||||||
|
export type CodeVerificationRecordData<T extends CodeVerificationType = CodeVerificationType> = {
|
||||||
|
id: string;
|
||||||
|
type: T;
|
||||||
|
identifier: VerificationCodeIdentifierOf<T>;
|
||||||
|
interactionEvent: InteractionEvent;
|
||||||
|
verified: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CodeVerification is a verification type that verifies a given identifier by sending a verification code
|
* This is the parent class for `EmailCodeVerification` and `PhoneCodeVerification`. Not publicly exposed.
|
||||||
* to the user's email or phone.
|
|
||||||
*
|
|
||||||
* @remark The verification code is sent to the user's email or phone and the user is required to enter the code to verify.
|
|
||||||
* If the identifier is for a existing user, the userId will be set after the verification.
|
|
||||||
*
|
|
||||||
* To avoid the redundant naming, the `CodeVerification` is used instead of `VerificationCodeVerification`.
|
|
||||||
*/
|
*/
|
||||||
export class CodeVerification
|
abstract class CodeVerification<T extends CodeVerificationType>
|
||||||
implements IdentifierVerificationRecord<VerificationType.VerificationCode>
|
implements IdentifierVerificationRecord<T>
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Factory method to create a new CodeVerification record using the given identifier.
|
|
||||||
*/
|
|
||||||
static create(
|
|
||||||
libraries: Libraries,
|
|
||||||
queries: Queries,
|
|
||||||
identifier: VerificationCodeIdentifier,
|
|
||||||
interactionEvent: InteractionEvent
|
|
||||||
) {
|
|
||||||
const record = new CodeVerification(libraries, queries, {
|
|
||||||
id: generateStandardId(),
|
|
||||||
type: VerificationType.VerificationCode,
|
|
||||||
identifier,
|
|
||||||
interactionEvent,
|
|
||||||
verified: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly id: string;
|
public readonly id: string;
|
||||||
public readonly type = VerificationType.VerificationCode;
|
public readonly identifier: VerificationCodeIdentifierOf<T>;
|
||||||
public readonly identifier: VerificationCodeIdentifier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interaction event that triggered the verification.
|
* The interaction event that triggered the verification.
|
||||||
|
@ -102,12 +84,13 @@ export class CodeVerification
|
||||||
* `InteractionEvent.ForgotPassword` triggered verification results can not used as a verification record for other events.
|
* `InteractionEvent.ForgotPassword` triggered verification results can not used as a verification record for other events.
|
||||||
*/
|
*/
|
||||||
public readonly interactionEvent: InteractionEvent;
|
public readonly interactionEvent: InteractionEvent;
|
||||||
private verified: boolean;
|
public abstract readonly type: T;
|
||||||
|
protected verified: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly libraries: Libraries,
|
private readonly libraries: Libraries,
|
||||||
private readonly queries: Queries,
|
private readonly queries: Queries,
|
||||||
data: CodeVerificationRecordData
|
data: CodeVerificationRecordData<T>
|
||||||
) {
|
) {
|
||||||
const { id, identifier, verified, interactionEvent } = data;
|
const { id, identifier, verified, interactionEvent } = data;
|
||||||
|
|
||||||
|
@ -144,7 +127,8 @@ export class CodeVerification
|
||||||
/**
|
/**
|
||||||
* Verify the `identifier` with the given code
|
* Verify the `identifier` with the given code
|
||||||
*
|
*
|
||||||
* @remark The identifier and code will be verified in the passcode library.
|
* @remarks
|
||||||
|
* The identifier and code will be verified in the passcode library.
|
||||||
* No need to verify the identifier before calling this method.
|
* No need to verify the identifier before calling this method.
|
||||||
*
|
*
|
||||||
* - `isVerified` will be set to true if the code is verified successfully.
|
* - `isVerified` will be set to true if the code is verified successfully.
|
||||||
|
@ -187,18 +171,7 @@ export class CodeVerification
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
toUserProfile(): { primaryEmail: string } | { primaryPhone: string } {
|
toJson(): CodeVerificationRecordData<T> {
|
||||||
assertThat(
|
|
||||||
this.verified,
|
|
||||||
new RequestError({ code: 'session.verification_failed', status: 400 })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { type, value } = this.identifier;
|
|
||||||
|
|
||||||
return type === 'email' ? { primaryEmail: value } : { primaryPhone: value };
|
|
||||||
}
|
|
||||||
|
|
||||||
toJson(): CodeVerificationRecordData {
|
|
||||||
const { id, type, identifier, interactionEvent, verified } = this;
|
const { id, type, identifier, interactionEvent, verified } = this;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -209,4 +182,113 @@ export class CodeVerification
|
||||||
verified,
|
verified,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract toUserProfile(): CodeVerificationIdentifierMap[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const basicCodeVerificationRecordDataGuard = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
interactionEvent: z.nativeEnum(InteractionEvent),
|
||||||
|
verified: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A verification code class that verifies a given email identifier.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The verification code is sent to the user's email and the user is required to enter the exact same code to
|
||||||
|
* complete the process. If the identifier is for an existing user, the `userId` will be set after the verification.
|
||||||
|
*/
|
||||||
|
export class EmailCodeVerification extends CodeVerification<VerificationType.EmailVerificationCode> {
|
||||||
|
public readonly type = VerificationType.EmailVerificationCode;
|
||||||
|
|
||||||
|
toUserProfile(): { primaryEmail: string } {
|
||||||
|
assertThat(
|
||||||
|
this.verified,
|
||||||
|
new RequestError({
|
||||||
|
code: 'session.verification_failed',
|
||||||
|
state: 400,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { value } = this.identifier;
|
||||||
|
|
||||||
|
return { primaryEmail: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emailCodeVerificationRecordDataGuard = basicCodeVerificationRecordDataGuard.extend({
|
||||||
|
type: z.literal(VerificationType.EmailVerificationCode),
|
||||||
|
identifier: z.object({
|
||||||
|
type: z.literal(SignInIdentifier.Email),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
}) satisfies ToZodObject<CodeVerificationRecordData<VerificationType.EmailVerificationCode>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A verification code class that verifies a given phone identifier.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The verification code will be sent to the user's phone and the user is required to enter the exact same code to
|
||||||
|
* complete the process. If the identifier is for an existing user, the `userId` will be set after the verification.
|
||||||
|
*/
|
||||||
|
export class PhoneCodeVerification extends CodeVerification<VerificationType.PhoneVerificationCode> {
|
||||||
|
public readonly type = VerificationType.PhoneVerificationCode;
|
||||||
|
|
||||||
|
toUserProfile(): { primaryPhone: string } {
|
||||||
|
assertThat(
|
||||||
|
this.verified,
|
||||||
|
new RequestError({
|
||||||
|
code: 'session.verification_failed',
|
||||||
|
state: 400,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { value } = this.identifier;
|
||||||
|
|
||||||
|
return { primaryPhone: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const phoneCodeVerificationRecordDataGuard = basicCodeVerificationRecordDataGuard.extend({
|
||||||
|
type: z.literal(VerificationType.PhoneVerificationCode),
|
||||||
|
identifier: z.object({
|
||||||
|
type: z.literal(SignInIdentifier.Phone),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
}) satisfies ToZodObject<CodeVerificationRecordData<VerificationType.PhoneVerificationCode>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a new `EmailCodeVerification` / `PhoneCodeVerification` record using the given identifier.
|
||||||
|
*/
|
||||||
|
export const createNewCodeVerificationRecord = (
|
||||||
|
libraries: Libraries,
|
||||||
|
queries: Queries,
|
||||||
|
identifier:
|
||||||
|
| VerificationCodeIdentifier<SignInIdentifier.Email>
|
||||||
|
| VerificationCodeIdentifier<SignInIdentifier.Phone>,
|
||||||
|
interactionEvent: InteractionEvent
|
||||||
|
) => {
|
||||||
|
const { type } = identifier;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case SignInIdentifier.Email: {
|
||||||
|
return new EmailCodeVerification(libraries, queries, {
|
||||||
|
id: generateStandardId(),
|
||||||
|
type: VerificationType.EmailVerificationCode,
|
||||||
|
identifier,
|
||||||
|
interactionEvent,
|
||||||
|
verified: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SignInIdentifier.Phone: {
|
||||||
|
return new PhoneCodeVerification(libraries, queries, {
|
||||||
|
id: generateStandardId(),
|
||||||
|
type: VerificationType.PhoneVerificationCode,
|
||||||
|
identifier,
|
||||||
|
interactionEvent,
|
||||||
|
verified: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -10,8 +10,10 @@ import {
|
||||||
type BackupCodeVerificationRecordData,
|
type BackupCodeVerificationRecordData,
|
||||||
} from './backup-code-verification.js';
|
} from './backup-code-verification.js';
|
||||||
import {
|
import {
|
||||||
CodeVerification,
|
EmailCodeVerification,
|
||||||
codeVerificationRecordDataGuard,
|
emailCodeVerificationRecordDataGuard,
|
||||||
|
PhoneCodeVerification,
|
||||||
|
phoneCodeVerificationRecordDataGuard,
|
||||||
type CodeVerificationRecordData,
|
type CodeVerificationRecordData,
|
||||||
} from './code-verification.js';
|
} from './code-verification.js';
|
||||||
import {
|
import {
|
||||||
|
@ -42,7 +44,8 @@ import {
|
||||||
|
|
||||||
export type VerificationRecordData =
|
export type VerificationRecordData =
|
||||||
| PasswordVerificationRecordData
|
| PasswordVerificationRecordData
|
||||||
| CodeVerificationRecordData
|
| CodeVerificationRecordData<VerificationType.EmailVerificationCode>
|
||||||
|
| CodeVerificationRecordData<VerificationType.PhoneVerificationCode>
|
||||||
| SocialVerificationRecordData
|
| SocialVerificationRecordData
|
||||||
| EnterpriseSsoVerificationRecordData
|
| EnterpriseSsoVerificationRecordData
|
||||||
| TotpVerificationRecordData
|
| TotpVerificationRecordData
|
||||||
|
@ -59,7 +62,8 @@ export type VerificationRecordData =
|
||||||
*/
|
*/
|
||||||
export type VerificationRecord =
|
export type VerificationRecord =
|
||||||
| PasswordVerification
|
| PasswordVerification
|
||||||
| CodeVerification
|
| EmailCodeVerification
|
||||||
|
| PhoneCodeVerification
|
||||||
| SocialVerification
|
| SocialVerification
|
||||||
| EnterpriseSsoVerification
|
| EnterpriseSsoVerification
|
||||||
| TotpVerification
|
| TotpVerification
|
||||||
|
@ -68,7 +72,8 @@ export type VerificationRecord =
|
||||||
|
|
||||||
export const verificationRecordDataGuard = z.discriminatedUnion('type', [
|
export const verificationRecordDataGuard = z.discriminatedUnion('type', [
|
||||||
passwordVerificationRecordDataGuard,
|
passwordVerificationRecordDataGuard,
|
||||||
codeVerificationRecordDataGuard,
|
emailCodeVerificationRecordDataGuard,
|
||||||
|
phoneCodeVerificationRecordDataGuard,
|
||||||
socialVerificationRecordDataGuard,
|
socialVerificationRecordDataGuard,
|
||||||
enterPriseSsoVerificationRecordDataGuard,
|
enterPriseSsoVerificationRecordDataGuard,
|
||||||
totpVerificationRecordDataGuard,
|
totpVerificationRecordDataGuard,
|
||||||
|
@ -88,8 +93,11 @@ export const buildVerificationRecord = (
|
||||||
case VerificationType.Password: {
|
case VerificationType.Password: {
|
||||||
return new PasswordVerification(libraries, queries, data);
|
return new PasswordVerification(libraries, queries, data);
|
||||||
}
|
}
|
||||||
case VerificationType.VerificationCode: {
|
case VerificationType.EmailVerificationCode: {
|
||||||
return new CodeVerification(libraries, queries, data);
|
return new EmailCodeVerification(libraries, queries, data);
|
||||||
|
}
|
||||||
|
case VerificationType.PhoneVerificationCode: {
|
||||||
|
return new PhoneCodeVerification(libraries, queries, data);
|
||||||
}
|
}
|
||||||
case VerificationType.Social: {
|
case VerificationType.Social: {
|
||||||
return new SocialVerification(libraries, queries, data);
|
return new SocialVerification(libraries, queries, data);
|
||||||
|
|
|
@ -19,7 +19,8 @@ export abstract class VerificationRecord<
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentifierVerificationType =
|
type IdentifierVerificationType =
|
||||||
| VerificationType.VerificationCode
|
| VerificationType.EmailVerificationCode
|
||||||
|
| VerificationType.PhoneVerificationCode
|
||||||
| VerificationType.Password
|
| VerificationType.Password
|
||||||
| VerificationType.Social
|
| VerificationType.Social
|
||||||
| VerificationType.EnterpriseSso;
|
| VerificationType.EnterpriseSso;
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { InteractionEvent, verificationCodeIdentifierGuard } from '@logto/schemas';
|
||||||
InteractionEvent,
|
|
||||||
VerificationType,
|
|
||||||
verificationCodeIdentifierGuard,
|
|
||||||
} from '@logto/schemas';
|
|
||||||
import type Router from 'koa-router';
|
import type Router from 'koa-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
@ -12,7 +8,8 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
|
|
||||||
import { CodeVerification } from '../classes/verifications/code-verification.js';
|
import { codeVerificationIdentifierRecordTypeMap } from '../classes/utils.js';
|
||||||
|
import { createNewCodeVerificationRecord } from '../classes/verifications/code-verification.js';
|
||||||
import { experienceRoutes } from '../const.js';
|
import { experienceRoutes } from '../const.js';
|
||||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||||
|
|
||||||
|
@ -36,7 +33,7 @@ export default function verificationCodeRoutes<T extends WithLogContext>(
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { identifier, interactionEvent } = ctx.guard.body;
|
const { identifier, interactionEvent } = ctx.guard.body;
|
||||||
|
|
||||||
const codeVerification = CodeVerification.create(
|
const codeVerification = createNewCodeVerificationRecord(
|
||||||
libraries,
|
libraries,
|
||||||
queries,
|
queries,
|
||||||
identifier,
|
identifier,
|
||||||
|
@ -80,7 +77,7 @@ export default function verificationCodeRoutes<T extends WithLogContext>(
|
||||||
assertThat(
|
assertThat(
|
||||||
codeVerificationRecord &&
|
codeVerificationRecord &&
|
||||||
// Make the Verification type checker happy
|
// Make the Verification type checker happy
|
||||||
codeVerificationRecord.type === VerificationType.VerificationCode,
|
codeVerificationRecord.type === codeVerificationIdentifierRecordTypeMap[identifier.type],
|
||||||
new RequestError({ code: 'session.verification_session_not_found', status: 404 })
|
new RequestError({ code: 'session.verification_session_not_found', status: 404 })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -41,12 +41,15 @@ export const interactionIdentifierGuard = z.object({
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
}) satisfies ToZodObject<InteractionIdentifier>;
|
}) satisfies ToZodObject<InteractionIdentifier>;
|
||||||
|
|
||||||
|
export type VerificationCodeSignInIdentifier = SignInIdentifier.Email | SignInIdentifier.Phone;
|
||||||
|
|
||||||
/** Currently only email and phone are supported for verification code validation. */
|
/** Currently only email and phone are supported for verification code validation. */
|
||||||
export type VerificationCodeIdentifier = {
|
export type VerificationCodeIdentifier<
|
||||||
type: SignInIdentifier.Email | SignInIdentifier.Phone;
|
T extends VerificationCodeSignInIdentifier = VerificationCodeSignInIdentifier,
|
||||||
|
> = {
|
||||||
|
type: T;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verificationCodeIdentifierGuard = z.object({
|
export const verificationCodeIdentifierGuard = z.object({
|
||||||
type: z.enum([SignInIdentifier.Email, SignInIdentifier.Phone]),
|
type: z.enum([SignInIdentifier.Email, SignInIdentifier.Phone]),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
|
@ -55,7 +58,8 @@ export const verificationCodeIdentifierGuard = z.object({
|
||||||
/** Logto supported interaction verification types. */
|
/** Logto supported interaction verification types. */
|
||||||
export enum VerificationType {
|
export enum VerificationType {
|
||||||
Password = 'Password',
|
Password = 'Password',
|
||||||
VerificationCode = 'VerificationCode',
|
EmailVerificationCode = 'EmailVerificationCode',
|
||||||
|
PhoneVerificationCode = 'PhoneVerificationCode',
|
||||||
Social = 'Social',
|
Social = 'Social',
|
||||||
EnterpriseSso = 'EnterpriseSso',
|
EnterpriseSso = 'EnterpriseSso',
|
||||||
TOTP = 'Totp',
|
TOTP = 'Totp',
|
||||||
|
|
Loading…
Add table
Reference in a new issue