Skip to content

Commit

Permalink
refactor(core,schemas): split CodeVerification type
Browse files Browse the repository at this point in the history
split CodeVerification type
  • Loading branch information
simeng-li committed Jul 20, 2024
1 parent 72891ec commit 9d4e069
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('ExperienceInteraction class', () => {

const emailVerificationRecord = new EmailCodeVerification(libraries, queries, {
id: 'mock_email_verification_id',
type: VerificationType.VerificationCode,
type: VerificationType.EmailVerificationCode,
identifier: {
type: SignInIdentifier.Email,
value: mockEmail,
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/routes/experience/classes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
VerificationType,
type InteractionIdentifier,
type User,
type VerificationCodeSignInIdentifier,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';

Expand Down Expand Up @@ -41,7 +42,8 @@ export const getNewUserProfileFromVerificationRecord = async (
): Promise<InteractionProfile> => {
switch (verificationRecord.type) {
case VerificationType.NewPasswordIdentity:
case VerificationType.VerificationCode: {
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
return verificationRecord.toUserProfile();
}
case VerificationType.EnterpriseSso:
Expand Down Expand Up @@ -86,7 +88,8 @@ export const identifyUserByVerificationRecord = async (

switch (verificationRecord.type) {
case VerificationType.Password:
case VerificationType.VerificationCode: {
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {

Check warning on line 92 in packages/core/src/routes/experience/classes/utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/utils.ts#L91-L92

Added lines #L91 - L92 were not covered by tests
return { user: await verificationRecord.identifyUser() };
}
case VerificationType.Social: {
Expand Down Expand Up @@ -171,3 +174,8 @@ export function profileToUserInfo(
phoneNumber: primaryPhone ?? undefined,
};
}

export const codeVerificationIdentifierRecordTypeMap = Object.freeze({
[SignInIdentifier.Email]: VerificationType.EmailVerificationCode,
[SignInIdentifier.Phone]: VerificationType.PhoneVerificationCode,
}) satisfies Record<VerificationCodeSignInIdentifier, VerificationType>;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PasswordPolicyChecker } from '@logto/core-kit';
import {
InteractionEvent,
type SignInExperience,
SignInIdentifier,
SignInMode,
VerificationType,
} from '@logto/schemas';
Expand All @@ -18,12 +19,13 @@ import { type VerificationRecord } from '../verifications/index.js';
const getEmailIdentifierFromVerificationRecord = (verificationRecord: VerificationRecord) => {
switch (verificationRecord.type) {
case VerificationType.Password:
case VerificationType.VerificationCode: {
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
const {
identifier: { type, value },
} = verificationRecord;

return type === 'email' ? value : undefined;
return type === SignInIdentifier.Email ? value : undefined;
}
case VerificationType.Social: {
const { socialUserInfo } = verificationRecord;
Expand Down Expand Up @@ -174,7 +176,8 @@ export class SignInExperienceValidator {

switch (verificationRecord.type) {
case VerificationType.Password:
case VerificationType.VerificationCode: {
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
const {
identifier: { type },
} = verificationRecord;
Expand Down Expand Up @@ -224,7 +227,8 @@ export class SignInExperienceValidator {
);
break;
}
case VerificationType.VerificationCode: {
case VerificationType.EmailVerificationCode:
case VerificationType.PhoneVerificationCode: {
const {
identifier: { type },
} = verificationRecord;
Expand Down Expand Up @@ -255,7 +259,8 @@ export class SignInExperienceValidator {
/** Forgot password only supports verification code type verification record */
private guardForgotPasswordVerificationMethod(verificationRecord: VerificationRecord) {
assertThat(
verificationRecord.type === VerificationType.VerificationCode,
verificationRecord.type === VerificationType.EmailVerificationCode ||
verificationRecord.type === VerificationType.PhoneVerificationCode,

Check warning on line 263 in packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/validators/sign-in-experience-validator.ts#L262-L263

Added lines #L262 - L263 were not covered by tests
new RequestError({ code: 'session.not_supported_for_forgot_password', status: 422 })
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { TemplateType } from '@logto/connector-kit';
import { TemplateType, type ToZodObject } from '@logto/connector-kit';
import {
InteractionEvent,
SignInIdentifier,
VerificationType,
verificationCodeIdentifierGuard,
type User,
type VerificationCodeIdentifier,
type VerificationCodeSignInIdentifier,
} from '@logto/schemas';
import { type ToZodObject } from '@logto/schemas/lib/utils/zod.js';
import { generateStandardId } from '@logto/shared';
import { z } from 'zod';

Expand Down Expand Up @@ -43,35 +40,36 @@ const getPasscodeIdentifierPayload = (
): Parameters<ReturnType<typeof createPasscodeLibrary>['createPasscode']>[2] =>
identifier.type === 'email' ? { email: identifier.value } : { phone: identifier.value };

type CodeVerificationType =
| VerificationType.EmailVerificationCode
| VerificationType.PhoneVerificationCode;

type SinInIdentifierTypeOf<T extends CodeVerificationType> =
T extends VerificationType.EmailVerificationCode
? SignInIdentifier.Email
: SignInIdentifier.Phone;

type VerificationCodeIdentifierOf<T extends CodeVerificationType> = VerificationCodeIdentifier<
SinInIdentifierTypeOf<T>
>;

/** The JSON data type for the CodeVerification record */
export type CodeVerificationRecordData<
T extends VerificationCodeSignInIdentifier = VerificationCodeSignInIdentifier,
> = {
export type CodeVerificationRecordData<T extends CodeVerificationType = CodeVerificationType> = {
id: string;
type: VerificationType.VerificationCode;
identifier: VerificationCodeIdentifier<T>;
type: T;
identifier: VerificationCodeIdentifierOf<T>;
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>;

/**
* 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.
*/
class CodeVerification<
T extends VerificationCodeSignInIdentifier = VerificationCodeSignInIdentifier,
> implements IdentifierVerificationRecord<VerificationType.VerificationCode>
abstract class CodeVerification<T extends CodeVerificationType>
implements IdentifierVerificationRecord<T>
{
public readonly id: string;
public readonly type = VerificationType.VerificationCode;
public readonly identifier: VerificationCodeIdentifier<T>;
public readonly identifier: VerificationCodeIdentifierOf<T>;

/**
* The interaction event that triggered the verification.
Expand All @@ -81,6 +79,7 @@ class CodeVerification<
* `InteractionEvent.ForgotPassword` triggered verification results can not used as a verification record for other events.
*/
public readonly interactionEvent: InteractionEvent;
public abstract readonly type: T;
protected verified: boolean;

constructor(
Expand Down Expand Up @@ -166,17 +165,6 @@ class CodeVerification<
return user;
}

toUserProfile(): { primaryEmail: string } | { primaryPhone: string } {
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<T> {
const { id, type, identifier, interactionEvent, verified } = this;

Expand All @@ -188,16 +176,28 @@ class CodeVerification<
verified,
};
}

abstract toUserProfile(): T extends VerificationType.EmailVerificationCode
? { primaryEmail: string }
: { primaryPhone: string };
}

const basicCodeVerificationRecordDataGuard = z.object({
id: z.string(),
interactionEvent: z.nativeEnum(InteractionEvent),
verified: z.boolean(),
});

/**
* CodeVerification is a verification type that verifies a given identifier by sending a verification code.
* EmailCodeVerification is a verification type that verifies a given email identifier by sending a verification code.
*
* @remark The verification code is sent to the user's email 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.
*/
export class EmailCodeVerification extends CodeVerification<SignInIdentifier.Email> {
override toUserProfile(): { primaryEmail: string } {
export class EmailCodeVerification extends CodeVerification<VerificationType.EmailVerificationCode> {
public readonly type = VerificationType.EmailVerificationCode;

toUserProfile(): { primaryEmail: string } {
assertThat(
this.verified,
new RequestError({
Expand All @@ -212,14 +212,24 @@ export class EmailCodeVerification extends CodeVerification<SignInIdentifier.Ema
}
}

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>>;

/**
* CodeVerification is a verification type that verifies a given identifier by sending a verification code.
* PhoneCodeVerification is a verification type that verifies a given phone identifier by sending a verification code.
*
* @remark The verification code is sent to the user's phone 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.
*/
export class PhoneCodeVerification extends CodeVerification<SignInIdentifier.Phone> {
override toUserProfile(): { primaryPhone: string } {
export class PhoneCodeVerification extends CodeVerification<VerificationType.PhoneVerificationCode> {
public readonly type = VerificationType.PhoneVerificationCode;

toUserProfile(): { primaryPhone: string } {
assertThat(
this.verified,
new RequestError({
Expand All @@ -234,15 +244,13 @@ export class PhoneCodeVerification extends CodeVerification<SignInIdentifier.Pho
}

Check warning on line 244 in packages/core/src/routes/experience/classes/verifications/code-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L233-L244

Added lines #L233 - L244 were not covered by tests
}

export const assertEmailCodeVerificationData = (
data: CodeVerificationRecordData
): data is CodeVerificationRecordData<SignInIdentifier.Email> =>
data.identifier.type === SignInIdentifier.Email;

export const assertPhoneCodeVerificationData = (
data: CodeVerificationRecordData
): data is CodeVerificationRecordData<SignInIdentifier.Phone> =>
data.identifier.type === SignInIdentifier.Phone;
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.
Expand All @@ -260,7 +268,7 @@ export const createNewCodeVerificationRecord = (
if (type === SignInIdentifier.Email) {
return new EmailCodeVerification(libraries, queries, {
id: generateStandardId(),
type: VerificationType.VerificationCode,
type: VerificationType.EmailVerificationCode,
identifier,
interactionEvent,
verified: false,
Expand All @@ -269,7 +277,7 @@ export const createNewCodeVerificationRecord = (

return new PhoneCodeVerification(libraries, queries, {
id: generateStandardId(),
type: VerificationType.VerificationCode,
type: VerificationType.PhoneVerificationCode,
identifier,
interactionEvent,
verified: false,
Expand Down
27 changes: 11 additions & 16 deletions packages/core/src/routes/experience/classes/verifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {
type BackupCodeVerificationRecordData,
} from './backup-code-verification.js';
import {
assertEmailCodeVerificationData,
assertPhoneCodeVerificationData,
codeVerificationRecordDataGuard,
EmailCodeVerification,
emailCodeVerificationRecordDataGuard,
PhoneCodeVerification,
phoneCodeVerificationRecordDataGuard,
type CodeVerificationRecordData,
} from './code-verification.js';
import {
Expand Down Expand Up @@ -45,7 +44,8 @@ import {

export type VerificationRecordData =
| PasswordVerificationRecordData
| CodeVerificationRecordData
| CodeVerificationRecordData<VerificationType.EmailVerificationCode>
| CodeVerificationRecordData<VerificationType.PhoneVerificationCode>
| SocialVerificationRecordData
| EnterpriseSsoVerificationRecordData
| TotpVerificationRecordData
Expand All @@ -72,7 +72,8 @@ export type VerificationRecord =

export const verificationRecordDataGuard = z.discriminatedUnion('type', [
passwordVerificationRecordDataGuard,
codeVerificationRecordDataGuard,
emailCodeVerificationRecordDataGuard,
phoneCodeVerificationRecordDataGuard,
socialVerificationRecordDataGuard,
enterPriseSsoVerificationRecordDataGuard,
totpVerificationRecordDataGuard,
Expand All @@ -92,17 +93,11 @@ export const buildVerificationRecord = (
case VerificationType.Password: {
return new PasswordVerification(libraries, queries, data);
}
case VerificationType.VerificationCode: {
// TS can't distribute the CodeVerificationRecordData type directly
// so we need to assert the data type here
if (assertEmailCodeVerificationData(data)) {
return new EmailCodeVerification(libraries, queries, data);
}
if (assertPhoneCodeVerificationData(data)) {
return new PhoneCodeVerification(libraries, queries, data);
}
// Make the type checker happy
throw new Error('Invalid code verification data');
case VerificationType.EmailVerificationCode: {
return new EmailCodeVerification(libraries, queries, data);
}
case VerificationType.PhoneVerificationCode: {
return new PhoneCodeVerification(libraries, queries, data);

Check warning on line 100 in packages/core/src/routes/experience/classes/verifications/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/index.ts#L96-L100

Added lines #L96 - L100 were not covered by tests
}
case VerificationType.Social: {
return new SocialVerification(libraries, queries, data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export abstract class VerificationRecord<
}

type IdentifierVerificationType =
| VerificationType.VerificationCode
| VerificationType.EmailVerificationCode
| VerificationType.PhoneVerificationCode

Check warning on line 23 in packages/core/src/routes/experience/classes/verifications/verification-record.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/verification-record.ts#L22-L23

Added lines #L22 - L23 were not covered by tests
| VerificationType.Password
| VerificationType.Social
| VerificationType.EnterpriseSso;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
InteractionEvent,
VerificationType,
verificationCodeIdentifierGuard,
} from '@logto/schemas';
import { InteractionEvent, verificationCodeIdentifierGuard } from '@logto/schemas';
import type Router from 'koa-router';
import { z } from 'zod';

Expand All @@ -12,6 +8,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';

import { codeVerificationIdentifierRecordTypeMap } from '../classes/utils.js';
import { createNewCodeVerificationRecord } from '../classes/verifications/code-verification.js';
import { experienceRoutes } from '../const.js';
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
Expand Down Expand Up @@ -80,8 +77,7 @@ export default function verificationCodeRoutes<T extends WithLogContext>(
assertThat(
codeVerificationRecord &&
// Make the Verification type checker happy
codeVerificationRecord.type === VerificationType.VerificationCode &&
codeVerificationRecord.identifier.type === identifier.type,
codeVerificationRecord.type === codeVerificationIdentifierRecordTypeMap[identifier.type],

Check warning on line 80 in packages/core/src/routes/experience/verification-routes/verification-code.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/verification-routes/verification-code.ts#L80

Added line #L80 was not covered by tests
new RequestError({ code: 'session.verification_session_not_found', status: 404 })
);

Expand Down
Loading

0 comments on commit 9d4e069

Please sign in to comment.