fix: RateLimit verify email and fix (#23718)
* fix: RateLimit verify email * update * client chnages * type error * fix * add test * Update event-types.e2e.ts * minor update * Update event-types.e2e.ts * fix test * handle it in api v2
This commit is contained in:
@@ -176,6 +176,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
@@ -471,6 +472,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
@@ -600,6 +602,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
guests: [],
|
||||
responses: { ...bookingResponses },
|
||||
rescheduleUid: inputBooking.seatUid,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -661,6 +664,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
guests: bookingResponses.guests,
|
||||
responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason },
|
||||
rescheduleUid: bookingUid,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3635,6 +3635,8 @@
|
||||
"user_name": "User Name",
|
||||
"expand_panel": "Expand Panel",
|
||||
"collapse_panel": "Collapse Panel",
|
||||
"email_verification_required": "Email verification is required for this event type",
|
||||
"invalid_verification_code": "Invalid verification code provided",
|
||||
"you_have_one_team": "You have one team",
|
||||
"consider_consolidating_one_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your team.",
|
||||
"consider_consolidating_multi_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your teams.",
|
||||
|
||||
@@ -62,6 +62,12 @@ export type BookerStore = {
|
||||
*/
|
||||
verifiedEmail: string | null;
|
||||
setVerifiedEmail: (email: string | null) => void;
|
||||
/**
|
||||
* Verification code for email verification.
|
||||
* Stored after successful verification to be included in booking request
|
||||
*/
|
||||
verificationCode: string | null;
|
||||
setVerificationCode: (code: string | null) => void;
|
||||
/**
|
||||
* Current month being viewed. Format is YYYY-MM.
|
||||
*/
|
||||
@@ -251,6 +257,10 @@ export const createBookerStore = () =>
|
||||
setVerifiedEmail: (email: string | null) => {
|
||||
set({ verifiedEmail: email });
|
||||
},
|
||||
verificationCode: null,
|
||||
setVerificationCode: (code: string | null) => {
|
||||
set({ verificationCode: code });
|
||||
},
|
||||
month:
|
||||
getQueryParam("month") ||
|
||||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import useDigitInput from "react-digit-input";
|
||||
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
@@ -36,6 +37,7 @@ export const VerifyCodeDialog = ({
|
||||
const { t } = useLocale();
|
||||
const [value, setValue] = useState("");
|
||||
const [hasVerified, setHasVerified] = useState(false);
|
||||
const setVerificationCode = useBookerStoreContext((state) => state.setVerificationCode);
|
||||
|
||||
const digits = useDigitInput({
|
||||
acceptedCharacters: /^[0-9]$/,
|
||||
@@ -56,6 +58,7 @@ export const VerifyCodeDialog = ({
|
||||
} else {
|
||||
verifyCodeWithSessionNotRequired(value, email);
|
||||
}
|
||||
setVerificationCode(value);
|
||||
setHasVerified(true);
|
||||
}, [
|
||||
resetErrors,
|
||||
|
||||
@@ -31,6 +31,7 @@ export type BookingOptions = {
|
||||
orgSlug?: string;
|
||||
routingFormSearchParams?: RoutingFormSearchParams;
|
||||
isDryRunProp?: boolean;
|
||||
verificationCode?: string;
|
||||
};
|
||||
|
||||
export const mapBookingToMutationInput = ({
|
||||
@@ -54,6 +55,7 @@ export const mapBookingToMutationInput = ({
|
||||
orgSlug,
|
||||
routingFormSearchParams,
|
||||
isDryRunProp,
|
||||
verificationCode,
|
||||
}: BookingOptions): BookingCreateBody => {
|
||||
const searchParams = new URLSearchParams(routingFormSearchParams ?? window.location.search);
|
||||
const routedTeamMemberIds = getRoutedTeamMemberIdsFromSearchParams(searchParams);
|
||||
@@ -98,6 +100,7 @@ export const mapBookingToMutationInput = ({
|
||||
_isDryRun,
|
||||
_shouldServeCache,
|
||||
dub_id,
|
||||
verificationCode,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export const bookingCreateBodySchema = z.object({
|
||||
.optional(),
|
||||
dub_id: z.string().nullish(),
|
||||
creationSource: z.nativeEnum(CreationSource).optional(),
|
||||
verificationCode: z.string().optional(),
|
||||
});
|
||||
|
||||
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
CreationSource,
|
||||
} from "@calcom/prisma/enums";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { verifyCodeUnAuthenticated } from "@calcom/trpc/server/routers/viewer/auth/util";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type {
|
||||
AdditionalInformation,
|
||||
@@ -499,6 +500,25 @@ async function handler(
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType.requiresBookerEmailVerification) {
|
||||
const verificationCode = reqBody.verificationCode;
|
||||
if (!verificationCode) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "email_verification_required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyCodeUnAuthenticated(bookerEmail, verificationCode);
|
||||
} catch (error) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "invalid_verification_code",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isEventTypeLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) {
|
||||
logger.settings.minLevel = 0;
|
||||
}
|
||||
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* These tests are unit tests that test the email verification functionality in the booking flow
|
||||
* - Test missing verification code scenario
|
||||
* - Test invalid verification code scenario
|
||||
* - Test successful verification scenario
|
||||
* - Test rate limiting behavior
|
||||
*/
|
||||
import {
|
||||
createBookingScenario,
|
||||
getBooker,
|
||||
getOrganizer,
|
||||
TestData,
|
||||
getScenarioData,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
import { vi, describe, expect, beforeEach } from "vitest";
|
||||
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
vi.mock("@calcom/trpc/server/routers/viewer/auth/util", () => ({
|
||||
verifyCodeUnAuthenticated: vi.fn(),
|
||||
}));
|
||||
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
describe("handleNewBooking - Email Verification", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Email Verification Required", () => {
|
||||
test(
|
||||
"should throw error when email verification is required but no verification code is provided",
|
||||
async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
requiresBookerEmailVerification: true,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(handleNewBooking({ bookingData: mockBookingData })).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
statusCode: 400,
|
||||
message: "email_verification_required",
|
||||
})
|
||||
);
|
||||
|
||||
expect(verifyCodeUnAuthenticated).not.toHaveBeenCalled();
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should throw error when email verification is required and verification code is invalid",
|
||||
async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
|
||||
|
||||
vi.mocked(verifyCodeUnAuthenticated).mockRejectedValue(new Error("Invalid verification code"));
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
requiresBookerEmailVerification: true,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
verificationCode: "invalid-code",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(handleNewBooking({ bookingData: mockBookingData })).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
statusCode: 400,
|
||||
message: "invalid_verification_code",
|
||||
})
|
||||
);
|
||||
|
||||
expect(verifyCodeUnAuthenticated).toHaveBeenCalledWith(booker.email, "invalid-code");
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should successfully create booking when email verification is required and valid verification code is provided",
|
||||
async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
|
||||
|
||||
vi.mocked(verifyCodeUnAuthenticated).mockResolvedValue(undefined);
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
requiresBookerEmailVerification: true,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
verificationCode: "valid-code",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await handleNewBooking({ bookingData: mockBookingData });
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.uid).toBeDefined();
|
||||
|
||||
expect(verifyCodeUnAuthenticated).toHaveBeenCalledWith(booker.email, "valid-code");
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should handle rate limiting error from verification service",
|
||||
async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
|
||||
|
||||
const rateLimitError = new Error("Rate limit exceeded");
|
||||
rateLimitError.name = "RateLimitError";
|
||||
vi.mocked(verifyCodeUnAuthenticated).mockRejectedValue(rateLimitError);
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
requiresBookerEmailVerification: true,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
verificationCode: "some-code",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(handleNewBooking({ bookingData: mockBookingData })).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
statusCode: 400,
|
||||
message: "invalid_verification_code",
|
||||
})
|
||||
);
|
||||
|
||||
expect(verifyCodeUnAuthenticated).toHaveBeenCalledWith(booker.email, "some-code");
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
"should proceed normally when email verification is not required",
|
||||
async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const { verifyCodeUnAuthenticated } = await import("@calcom/trpc/server/routers/viewer/auth/util");
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
requiresBookerEmailVerification: false,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "integrations:daily" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await handleNewBooking({ bookingData: mockBookingData });
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.uid).toBeDefined();
|
||||
|
||||
expect(verifyCodeUnAuthenticated).not.toHaveBeenCalled();
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,9 @@ export interface PiiHasher {
|
||||
export class Md5PiiHasher implements PiiHasher {
|
||||
constructor(private readonly salt: string) {}
|
||||
hash(input: string) {
|
||||
return createHash("md5").update(this.salt + input).digest("hex");
|
||||
return createHash("md5")
|
||||
.update(this.salt + input)
|
||||
.digest("hex");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +18,5 @@ export const piiHasher: PiiHasher = new Md5PiiHasher(process.env.CALENDSO_ENCRYP
|
||||
export const hashEmail = (email: string, hasher: PiiHasher = piiHasher): string => {
|
||||
const [localPart, domain] = email.split("@");
|
||||
// Simple hash function for email, can be replaced with a more complex one if needed
|
||||
return hasher.hash(localPart) + "@" + domain;
|
||||
}
|
||||
return `${hasher.hash(localPart)}@${domain}`;
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ export const useHandleBookEvent = ({
|
||||
const crmOwnerRecordType = useBookerStoreContext((state) => state.crmOwnerRecordType);
|
||||
const crmAppSlug = useBookerStoreContext((state) => state.crmAppSlug);
|
||||
const crmRecordId = useBookerStoreContext((state) => state.crmRecordId);
|
||||
const verificationCode = useBookerStoreContext((state) => state.verificationCode);
|
||||
const handleError = (err: any) => {
|
||||
const errorMessage = err?.message ? t(err.message) : t("can_you_try_again");
|
||||
showToast(errorMessage, "error");
|
||||
@@ -113,6 +114,7 @@ export const useHandleBookEvent = ({
|
||||
orgSlug: orgSlug ? orgSlug : undefined,
|
||||
routingFormSearchParams,
|
||||
isDryRunProp: isBookingDryRun,
|
||||
verificationCode: verificationCode || undefined,
|
||||
};
|
||||
|
||||
const tracking = getUtmTrackingParameters(searchParams);
|
||||
|
||||
@@ -371,6 +371,15 @@ export class CreateBookingInput_2024_08_13 {
|
||||
@ValidateNested()
|
||||
@Type(() => Routing)
|
||||
routing?: Routing;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Email verification code required when event type has email verification enabled.",
|
||||
example: "123456",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
emailVerificationCode?: string;
|
||||
}
|
||||
|
||||
export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 {
|
||||
|
||||
@@ -27,6 +27,15 @@ export class RescheduleBookingInput_2024_08_13 {
|
||||
description: "Reason for rescheduling the booking",
|
||||
})
|
||||
reschedulingReason?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Email verification code required when event type has email verification enabled.",
|
||||
example: "123456",
|
||||
})
|
||||
emailVerificationCode?: string;
|
||||
}
|
||||
|
||||
export class RescheduleSeatedBookingInput_2024_08_13 {
|
||||
@@ -51,4 +60,13 @@ export class RescheduleSeatedBookingInput_2024_08_13 {
|
||||
})
|
||||
@IsString()
|
||||
seatUid!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Email verification code required when event type has email verification enabled.",
|
||||
example: "123456",
|
||||
})
|
||||
emailVerificationCode?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { hashEmail } from "@calcom/lib/server/PiiHasher";
|
||||
import { totpRawCheck } from "@calcom/lib/totp";
|
||||
|
||||
export const verifyCodeUnAuthenticated = async (email: string, code: string) => {
|
||||
if (!email || !code) {
|
||||
throw new Error("Email and code are required");
|
||||
}
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: `emailVerifyCode.${hashEmail(email)}`,
|
||||
});
|
||||
|
||||
const secret = createHash("md5")
|
||||
.update(email + (process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||
.digest("hex");
|
||||
|
||||
const isValidToken = totpRawCheck(code, secret, { step: 900 });
|
||||
|
||||
if (!isValidToken) {
|
||||
throw new Error("Invalid verification code");
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
@@ -1,30 +1,19 @@
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { totpRawCheck } from "@calcom/lib/totp";
|
||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { verifyCodeUnAuthenticated } from "./util";
|
||||
|
||||
type VerifyTokenOptions = {
|
||||
input: ZVerifyCodeInputSchema;
|
||||
};
|
||||
|
||||
export const verifyCodeUnAuthenticatedHandler = async ({ input }: VerifyTokenOptions) => {
|
||||
return verifyCodeUnAuthenticated(input);
|
||||
};
|
||||
|
||||
export const verifyCodeUnAuthenticated = async (input: ZVerifyCodeInputSchema) => {
|
||||
const { email, code } = input;
|
||||
|
||||
if (!email || !code) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
const secret = createHash("md5")
|
||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
.digest("hex");
|
||||
|
||||
const isValidToken = totpRawCheck(code, secret, { step: 900 });
|
||||
|
||||
if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
||||
|
||||
return isValidToken;
|
||||
try {
|
||||
await verifyCodeUnAuthenticated(email, code);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user