feat: Verify email of people setting a meeting with you (#10317)
* booker email verification changes * name type fix * use totp and code * prisma schema styling * refactor: code Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: book event form Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: type error Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: type errors Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: unit tests Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * refactor: move verifycodedialog from ui and to features/bookings * fix: type error --------- Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: rkreddy99 <rreddy@e2clouds.com> Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
This commit is contained in:
parent
fcd8de43d6
commit
5a430df5d9
|
@ -204,6 +204,21 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
onRequiresConfirmation={setRequiresConfirmation}
|
||||
/>
|
||||
<hr className="border-subtle" />
|
||||
<Controller
|
||||
name="requiresBookerEmailVerification"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.requiresBookerEmailVerification}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
title={t("requires_booker_email_verification")}
|
||||
{...shouldLockDisableProps("requiresBookerEmailVerification")}
|
||||
description={t("description_requires_booker_email_verification")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<hr className="border-subtle" />
|
||||
<Controller
|
||||
name="hideCalendarNotes"
|
||||
control={formMethods.control}
|
||||
|
|
|
@ -217,6 +217,7 @@ export type UserPageProps = {
|
|||
| "length"
|
||||
| "hidden"
|
||||
| "requiresConfirmation"
|
||||
| "requiresBookerEmailVerification"
|
||||
| "price"
|
||||
| "currency"
|
||||
| "recurringEvent"
|
||||
|
|
|
@ -87,6 +87,7 @@ export type FormValues = {
|
|||
description: string;
|
||||
disableGuests: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
requiresBookerEmailVerification: boolean;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
schedulingType: SchedulingType | null;
|
||||
hidden: boolean;
|
||||
|
|
|
@ -19,8 +19,13 @@
|
|||
"verify_email_email_button": "Verify email",
|
||||
"copy_somewhere_safe": "Save this API key somewhere safe. You will not be able to view it again.",
|
||||
"verify_email_email_body": "Please verify your email address by clicking the button below.",
|
||||
"verify_email_by_code_email_body": "Please verify your email address by using the below code.",
|
||||
"verify_email_email_link_text": "Here's the link in case you don't like clicking buttons:",
|
||||
"email_verification_code": "Enter verification code",
|
||||
"email_verification_code_placeholder": "Enter verification code sent to your mail",
|
||||
"incorrect_email_verification_code": "Verification code is incorrect.",
|
||||
"email_sent": "Email sent successfully",
|
||||
"email_not_sent": "Error occurred while sending email",
|
||||
"event_declined_subject": "Declined: {{title}} at {{date}}",
|
||||
"event_cancelled_subject": "Canceled: {{title}} at {{date}}",
|
||||
"event_request_declined": "Your event request has been declined",
|
||||
|
@ -1965,5 +1970,7 @@
|
|||
"org_team_names_example_5": "e.g. Data Analytics Team",
|
||||
"org_max_team_warnings": "You will be able to add more teams later on.",
|
||||
"what_is_this_meeting_about": "What is this meeting about?",
|
||||
"requires_booker_email_verification": "Requires booker email verification",
|
||||
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ describe("handleChildrenEventTypes", () => {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, users, ...evType } = mockFindFirstEventType({
|
||||
const { schedulingType, id, teamId, timeZone, users,requiresBookerEmailVerification, ...evType } = mockFindFirstEventType({
|
||||
id: 123,
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
|
@ -133,7 +133,7 @@ describe("handleChildrenEventTypes", () => {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, locations, parentId, userId, scheduleId, ...evType } =
|
||||
const { schedulingType, id, teamId, timeZone, locations, parentId, userId, scheduleId,requiresBookerEmailVerification, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
|
@ -218,7 +218,7 @@ describe("handleChildrenEventTypes", () => {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, users, ...evType } = mockFindFirstEventType({
|
||||
const { schedulingType, id, teamId, timeZone, users,requiresBookerEmailVerification, ...evType } = mockFindFirstEventType({
|
||||
id: 123,
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
|
@ -255,7 +255,7 @@ describe("handleChildrenEventTypes", () => {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, users, locations, parentId, userId, ...evType } =
|
||||
const { schedulingType, id, teamId, timeZone, users, locations, parentId, userId,requiresBookerEmailVerification, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
|
@ -303,6 +303,7 @@ describe("handleChildrenEventTypes", () => {
|
|||
timeZone: _timeZone,
|
||||
parentId: _parentId,
|
||||
userId: _userId,
|
||||
requiresBookerEmailVerification,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
|
|
|
@ -42,6 +42,7 @@ import OrganizerScheduledEmail from "./templates/organizer-scheduled-email";
|
|||
import SlugReplacementEmail from "./templates/slug-replacement-email";
|
||||
import type { TeamInvite } from "./templates/team-invite-email";
|
||||
import TeamInviteEmail from "./templates/team-invite-email";
|
||||
import AttendeeVerifyEmail, { EmailVerifyCode } from "./templates/attendee-verify-email";
|
||||
|
||||
const sendEmail = (prepare: () => BaseEmail) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -275,6 +276,10 @@ export const sendEmailVerificationLink = async (verificationInput: EmailVerifyLi
|
|||
await sendEmail(() => new AccountVerifyEmail(verificationInput));
|
||||
};
|
||||
|
||||
export const sendEmailVerificationCode = async (verificationInput: EmailVerifyCode) => {
|
||||
await sendEmail(() => new AttendeeVerifyEmail(verificationInput));
|
||||
};
|
||||
|
||||
export const sendRequestRescheduleEmail = async (
|
||||
calEvent: CalendarEvent,
|
||||
metadata: { rescheduleLink: string }
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
|
||||
import type { EmailVerifyCode } from "../../templates/attendee-verify-email";
|
||||
import { BaseEmailHtml } from "../components";
|
||||
|
||||
export const VerifyEmailByCode = (
|
||||
props: EmailVerifyCode & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml subject={props.language("verify_email_subject", { appName: APP_NAME })}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "32px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{props.language("verify_email_email_header")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{props.language("hi_user_name", { name: props.user.name })}!</>
|
||||
</p>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("verify_email_by_code_email_body")}</>
|
||||
<br />
|
||||
<p>{props.verificationEmailCode}</p>
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("happy_scheduling")}, <br />
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("the_calcom_team", { companyName: SENDER_NAME })}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
|
@ -24,6 +24,7 @@ export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
|
|||
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
|
||||
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
|
||||
export { VerifyAccountEmail } from "./VerifyAccountEmail";
|
||||
export { VerifyEmailByCode } from "./VerifyEmailByCode"
|
||||
export * from "@calcom/app-store/routing-forms/emails/components";
|
||||
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
|
||||
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, COMPANY_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import BaseEmail from "./_base-email";
|
||||
|
||||
export type EmailVerifyCode = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
name?: string | null;
|
||||
email: string;
|
||||
};
|
||||
verificationEmailCode: string;
|
||||
};
|
||||
|
||||
export default class AttendeeVerifyEmail extends BaseEmail {
|
||||
verifyAccountInput: EmailVerifyCode;
|
||||
|
||||
constructor(passwordEvent: EmailVerifyCode) {
|
||||
super();
|
||||
this.name = "SEND_ACCOUNT_VERIFY_EMAIL";
|
||||
this.verifyAccountInput = passwordEvent;
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
|
||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||
subject: this.verifyAccountInput.language("verify_email_subject", {
|
||||
appName: APP_NAME,
|
||||
}),
|
||||
html: renderEmail("VerifyEmailByCode", this.verifyAccountInput),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
return `
|
||||
${this.verifyAccountInput.language("verify_email_subject", { appName: APP_NAME })}
|
||||
${this.verifyAccountInput.language("verify_email_email_header")}
|
||||
${this.verifyAccountInput.language("hi_user_name", { name: this.verifyAccountInput.user.name })},
|
||||
${this.verifyAccountInput.language("verify_email_by_code_email_body")}
|
||||
${this.verifyAccountInput.verificationEmailCode}
|
||||
${this.verifyAccountInput.language("happy_scheduling")} ${this.verifyAccountInput.language(
|
||||
"the_calcom_team",
|
||||
{ companyName: COMPANY_NAME }
|
||||
)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ export enum ErrorCode {
|
|||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
IncorrectEmailVerificationCode = "incorrect_email_verification_code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { randomBytes } from "crypto";
|
||||
import { randomBytes, createHash } from "crypto";
|
||||
import { totp } from "otplib";
|
||||
|
||||
import { sendEmailVerificationLink } from "@calcom/emails/email-manager";
|
||||
import { sendEmailVerificationCode, sendEmailVerificationLink } from "@calcom/emails/email-manager";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
@ -54,3 +55,24 @@ export const sendEmailVerification = async ({ email, language, username }: Verif
|
|||
|
||||
return { ok: true, skipped: false };
|
||||
};
|
||||
|
||||
export const sendEmailVerificationByCode = async ({ email, language, username }: VerifyEmailType) => {
|
||||
const translation = await getTranslation(language ?? "en", "common");
|
||||
const secret = createHash("md5")
|
||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
.digest("hex");
|
||||
|
||||
totp.options = { step: 900 };
|
||||
const code = totp.generate(secret);
|
||||
|
||||
await sendEmailVerificationCode({
|
||||
language: translation,
|
||||
verificationEmailCode: code,
|
||||
user: {
|
||||
email,
|
||||
name: username,
|
||||
},
|
||||
});
|
||||
|
||||
return { ok: true, skipped: false };
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||
import { useSession } from "next-auth/react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
@ -12,6 +12,7 @@ import { z } from "zod";
|
|||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { VerifyCodeDialog } from "@calcom/features/bookings/components/VerifyCodeDialog";
|
||||
import {
|
||||
useTimePreferences,
|
||||
mapBookingToMutationInput,
|
||||
|
@ -29,7 +30,7 @@ import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Form, Button, Alert, EmptyScreen } from "@calcom/ui";
|
||||
import { Form, Button, Alert, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useBookerStore } from "../../store";
|
||||
|
@ -63,6 +64,8 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
|||
const formValues = useBookerStore((state) => state.formValues);
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
const seatedEventData = useBookerStore((state) => state.seatedEventData);
|
||||
const verifiedEmail = useBookerStore((state) => state.verifiedEmail);
|
||||
const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail);
|
||||
const isRescheduling = !!rescheduleUid && !!bookingData;
|
||||
const event = useEvent();
|
||||
const eventType = event.data;
|
||||
|
@ -269,6 +272,37 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
|||
},
|
||||
});
|
||||
|
||||
const [isEmailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false);
|
||||
const email = bookingForm.watch("responses.email");
|
||||
|
||||
const sendEmailVerificationByCodeMutation = trpc.viewer.auth.sendVerifyEmailCode.useMutation({
|
||||
onSuccess() {
|
||||
showToast(t("email_sent"), "success");
|
||||
},
|
||||
onError() {
|
||||
showToast(t("email_not_sent"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
const verifyEmail = () => {
|
||||
bookingForm.clearErrors();
|
||||
|
||||
// It shouldn't be possible that this method is fired without having event data,
|
||||
// but since in theory (looking at the types) it is possible, we still handle that case.
|
||||
if (!event?.data) {
|
||||
bookingForm.setError("globalError", { message: t("error_booking_event") });
|
||||
return;
|
||||
}
|
||||
|
||||
const name = bookingForm.getValues("responses.name");
|
||||
|
||||
sendEmailVerificationByCodeMutation.mutate({
|
||||
email,
|
||||
username: typeof name === "string" ? name : name.firstName,
|
||||
});
|
||||
setEmailVerificationModalVisible(true);
|
||||
};
|
||||
|
||||
if (event.isError) return <Alert severity="warning" message={t("error_booking_event")} />;
|
||||
if (event.isLoading || !event.data) return <FormSkeleton />;
|
||||
if (!timeslot)
|
||||
|
@ -338,6 +372,9 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
|||
return <Alert severity="warning" message={t("error_booking_event")} />;
|
||||
}
|
||||
|
||||
const renderConfirmNotVerifyEmailButtonCond =
|
||||
!eventType?.requiresBookerEmailVerification || (email && verifiedEmail && verifiedEmail === email);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<Form
|
||||
|
@ -350,7 +387,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
|||
setFormValues(values);
|
||||
}}
|
||||
form={bookingForm}
|
||||
handleSubmit={bookEvent}
|
||||
handleSubmit={renderConfirmNotVerifyEmailButtonCond ? bookEvent : verifyEmail}
|
||||
noValidate>
|
||||
<BookingFields
|
||||
isDynamicGroupBooking={!!(username && username.indexOf("+") > -1)}
|
||||
|
@ -387,10 +424,24 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
|||
color="primary"
|
||||
loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading}
|
||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}>
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
{rescheduleUid
|
||||
? t("reschedule")
|
||||
: renderConfirmNotVerifyEmailButtonCond
|
||||
? t("confirm")
|
||||
: t("verify_email_email_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
<VerifyCodeDialog
|
||||
isOpenDialog={isEmailVerificationModalVisible}
|
||||
setIsOpenDialog={setEmailVerificationModalVisible}
|
||||
email={email}
|
||||
onSuccess={() => {
|
||||
setVerifiedEmail(email);
|
||||
setEmailVerificationModalVisible(false);
|
||||
}}
|
||||
isUserSessionRequiredToVerify={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ type StoreInitializeType = {
|
|||
bookingUid?: string | null;
|
||||
isTeamEvent?: boolean;
|
||||
bookingData?: GetBookingType | null | undefined;
|
||||
verifiedEmail?: string | null;
|
||||
rescheduleUid?: string | null;
|
||||
seatReferenceUid?: string;
|
||||
org?: string | null;
|
||||
|
@ -41,6 +42,12 @@ export type BookerStore = {
|
|||
username: string | null;
|
||||
eventSlug: string | null;
|
||||
eventId: number | null;
|
||||
/**
|
||||
* Verified booker email.
|
||||
* Needed in case user turns on Requires Booker Email Verification for an event
|
||||
*/
|
||||
verifiedEmail: string | null;
|
||||
setVerifiedEmail: (email: string | null) => void;
|
||||
/**
|
||||
* Current month being viewed. Format is YYYY-MM.
|
||||
*/
|
||||
|
@ -173,6 +180,10 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
|||
username: null,
|
||||
eventSlug: null,
|
||||
eventId: null,
|
||||
verifiedEmail: null,
|
||||
setVerifiedEmail: (email: string | null) => {
|
||||
set({ verifiedEmail: email });
|
||||
},
|
||||
month: getQueryParam("month") || getQueryParam("date") || dayjs().format("YYYY-MM"),
|
||||
setMonth: (month: string | null) => {
|
||||
set({ month, selectedTimeslot: null });
|
||||
|
@ -268,6 +279,7 @@ export const useInitializeBookerStore = ({
|
|||
eventId,
|
||||
rescheduleUid = null,
|
||||
bookingData = null,
|
||||
verifiedEmail = null,
|
||||
layout,
|
||||
isTeamEvent,
|
||||
org,
|
||||
|
@ -284,6 +296,7 @@ export const useInitializeBookerStore = ({
|
|||
layout,
|
||||
isTeamEvent,
|
||||
org,
|
||||
verifiedEmail,
|
||||
});
|
||||
}, [
|
||||
initializeStore,
|
||||
|
@ -296,5 +309,6 @@ export const useInitializeBookerStore = ({
|
|||
bookingData,
|
||||
layout,
|
||||
isTeamEvent,
|
||||
verifiedEmail,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import useDigitInput from "react-digit-input";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Label,
|
||||
Input,
|
||||
} from "@calcom/ui";
|
||||
import { Info } from "@calcom/ui/components/icon";
|
||||
|
||||
export const VerifyCodeDialog = ({
|
||||
isOpenDialog,
|
||||
setIsOpenDialog,
|
||||
email,
|
||||
onSuccess,
|
||||
isUserSessionRequiredToVerify = true,
|
||||
}: {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
email: string;
|
||||
onSuccess: (isVerified: boolean) => void;
|
||||
isUserSessionRequiredToVerify?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
// Not using the mutation isLoading flag because after verifying we submit the underlying org creation form
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [value, onChange] = useState("");
|
||||
|
||||
const digits = useDigitInput({
|
||||
acceptedCharacters: /^[0-9]$/,
|
||||
length: 6,
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const verifyCodeMutationUserSessionRequired = trpc.viewer.organizations.verifyCode.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsLoading(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsLoading(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeMutationUserSessionNotRequired = trpc.viewer.auth.verifyCodeUnAuthenticated.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsLoading(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsLoading(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => onChange(""), [isOpenDialog]);
|
||||
|
||||
const digitClassName = "h-12 w-12 !text-xl text-center";
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
onChange("");
|
||||
setError("");
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full">
|
||||
<DialogHeader title={t("verify_your_email")} subtitle={t("enter_digit_code", { email })} />
|
||||
<Label htmlFor="code">{t("code")}</Label>
|
||||
<div className="flex flex-row justify-between">
|
||||
<Input
|
||||
className={digitClassName}
|
||||
name="2fa1"
|
||||
inputMode="decimal"
|
||||
{...digits[0]}
|
||||
autoFocus
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
<Input className={digitClassName} name="2fa2" inputMode="decimal" {...digits[1]} />
|
||||
<Input className={digitClassName} name="2fa3" inputMode="decimal" {...digits[2]} />
|
||||
<Input className={digitClassName} name="2fa4" inputMode="decimal" {...digits[3]} />
|
||||
<Input className={digitClassName} name="2fa5" inputMode="decimal" {...digits[4]} />
|
||||
<Input className={digitClassName} name="2fa6" inputMode="decimal" {...digits[5]} />
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mt-2 flex items-center gap-x-2 text-sm text-red-700">
|
||||
<div>
|
||||
<Info className="h-3 w-3" />
|
||||
</div>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
if (value === "") {
|
||||
setError("The code is a required field");
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
if (isUserSessionRequiredToVerify) {
|
||||
verifyCodeMutationUserSessionRequired.mutate({
|
||||
code: value,
|
||||
email,
|
||||
});
|
||||
} else {
|
||||
verifyCodeMutationUserSessionNotRequired.mutate({
|
||||
code: value,
|
||||
email,
|
||||
});
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{t("verify")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -259,6 +259,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
userId: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
|
@ -2384,6 +2385,7 @@ const findBookingQuery = async (bookingId: number) => {
|
|||
currency: true,
|
||||
length: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
price: true,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,29 +1,16 @@
|
|||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useState } from "react";
|
||||
import useDigitInput from "react-digit-input";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { VerifyCodeDialog } from "@calcom/features/bookings/components/VerifyCodeDialog";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
TextField,
|
||||
Alert,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Label,
|
||||
Input,
|
||||
} from "@calcom/ui";
|
||||
import { ArrowRight, Info } from "@calcom/ui/components/icon";
|
||||
import { Button, Form, TextField, Alert } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
function extractDomainFromEmail(email: string) {
|
||||
let out = "";
|
||||
|
@ -34,107 +21,6 @@ function extractDomainFromEmail(email: string) {
|
|||
return out.split(".")[0];
|
||||
}
|
||||
|
||||
export const VerifyCodeDialog = ({
|
||||
isOpenDialog,
|
||||
setIsOpenDialog,
|
||||
email,
|
||||
onSuccess,
|
||||
}: {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
email: string;
|
||||
onSuccess: (isVerified: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
// Not using the mutation isLoading flag because after verifying we submit the underlying org creation form
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [value, onChange] = useState("");
|
||||
|
||||
const digits = useDigitInput({
|
||||
acceptedCharacters: /^[0-9]$/,
|
||||
length: 6,
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const verifyCodeMutation = trpc.viewer.organizations.verifyCode.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsLoading(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsLoading(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const digitClassName = "h-12 w-12 !text-xl text-center";
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
onChange("");
|
||||
setError("");
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full">
|
||||
<DialogHeader title={t("verify_your_email")} subtitle={t("enter_digit_code", { email })} />
|
||||
<Label htmlFor="code">{t("code")}</Label>
|
||||
<div className="flex flex-row justify-between">
|
||||
<Input
|
||||
className={digitClassName}
|
||||
name="2fa1"
|
||||
inputMode="decimal"
|
||||
{...digits[0]}
|
||||
autoFocus
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
<Input className={digitClassName} name="2fa2" inputMode="decimal" {...digits[1]} />
|
||||
<Input className={digitClassName} name="2fa3" inputMode="decimal" {...digits[2]} />
|
||||
<Input className={digitClassName} name="2fa4" inputMode="decimal" {...digits[3]} />
|
||||
<Input className={digitClassName} name="2fa5" inputMode="decimal" {...digits[4]} />
|
||||
<Input className={digitClassName} name="2fa6" inputMode="decimal" {...digits[5]} />
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mt-2 flex items-center gap-x-2 text-sm text-red-700">
|
||||
<div>
|
||||
<Info className="h-3 w-3" />
|
||||
</div>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
if (value === "") {
|
||||
setError("The code is a required field");
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
verifyCodeMutation.mutate({
|
||||
code: value,
|
||||
email,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{t("verify")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateANewOrganizationForm = ({ slug }: { slug?: string }) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
|
|
|
@ -32,6 +32,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
|||
disableGuests: true,
|
||||
metadata: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
recurringEvent: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
|
|
|
@ -86,6 +86,7 @@ const commons = {
|
|||
destinationCalendar: null,
|
||||
team: null,
|
||||
requiresConfirmation: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
bookingLimits: null,
|
||||
durationLimits: null,
|
||||
hidden: false,
|
||||
|
|
|
@ -91,6 +91,7 @@ export default async function getEventTypeById({
|
|||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
recurringEvent: true,
|
||||
hideCalendarNotes: true,
|
||||
disableGuests: true,
|
||||
|
|
|
@ -76,6 +76,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
|||
hidden: false,
|
||||
userId: null,
|
||||
teamId: null,
|
||||
requiresBookerEmailVerification: false,
|
||||
eventName: faker.lorem.words(),
|
||||
timeZone: null,
|
||||
periodType: "UNLIMITED",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "requiresBookerEmailVerification" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -47,70 +47,71 @@ model Host {
|
|||
}
|
||||
|
||||
model EventType {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
/// @zod.min(1)
|
||||
title String
|
||||
title String
|
||||
/// @zod.custom(imports.eventTypeSlug)
|
||||
slug String
|
||||
description String?
|
||||
position Int @default(0)
|
||||
slug String
|
||||
description String?
|
||||
position Int @default(0)
|
||||
/// @zod.custom(imports.eventTypeLocations)
|
||||
locations Json?
|
||||
length Int
|
||||
offsetStart Int @default(0)
|
||||
hidden Boolean @default(false)
|
||||
hosts Host[]
|
||||
users User[] @relation("user_eventtype")
|
||||
owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
locations Json?
|
||||
length Int
|
||||
offsetStart Int @default(0)
|
||||
hidden Boolean @default(false)
|
||||
hosts Host[]
|
||||
users User[] @relation("user_eventtype")
|
||||
owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
/// @zod.custom(imports.eventTypeBookingFields)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
periodStartDate DateTime?
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
requiresConfirmation Boolean @default(false)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
periodStartDate DateTime?
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
/// @zod.custom(imports.recurringEventType)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
/// @zod.min(0)
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
|
||||
price Int @default(0)
|
||||
price Int @default(0)
|
||||
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
/// @zod.custom(imports.EventTypeMetaDataSchema)
|
||||
metadata Json?
|
||||
metadata Json?
|
||||
/// @zod.custom(imports.successRedirectUrl)
|
||||
successRedirectUrl String?
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
successRedirectUrl String?
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
bookingLimits Json?
|
||||
bookingLimits Json?
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
durationLimits Json?
|
||||
durationLimits Json?
|
||||
|
||||
@@unique([userId, slug])
|
||||
@@unique([teamId, slug])
|
||||
|
|
|
@ -12,6 +12,7 @@ export const baseEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
|||
price: true,
|
||||
currency: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
});
|
||||
|
||||
export const bookEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
|
@ -28,6 +29,7 @@ export const bookEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
|||
periodEndDate: true,
|
||||
recurringEvent: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
metadata: true,
|
||||
periodCountCalendarDays: true,
|
||||
price: true,
|
||||
|
|
|
@ -598,3 +598,10 @@ export const emailSchemaRefinement = (value: string) => {
|
|||
const emailRegex = /^([A-Z0-9_+-]+\.?)*[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i;
|
||||
return emailRegex.test(value);
|
||||
};
|
||||
|
||||
export const ZVerifyCodeInputSchema = z.object({
|
||||
email: z.string().email(),
|
||||
code: z.string(),
|
||||
});
|
||||
|
||||
export type ZVerifyCodeInputSchema = z.infer<typeof ZVerifyCodeInputSchema>;
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import authedProcedure from "../../../procedures/authedProcedure";
|
||||
import publicProcedure from "../../../procedures/publicProcedure";
|
||||
import { router } from "../../../trpc";
|
||||
import { ZChangePasswordInputSchema } from "./changePassword.schema";
|
||||
import { ZSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema";
|
||||
import { ZVerifyPasswordInputSchema } from "./verifyPassword.schema";
|
||||
|
||||
type AuthRouterHandlerCache = {
|
||||
changePassword?: typeof import("./changePassword.handler").changePasswordHandler;
|
||||
verifyPassword?: typeof import("./verifyPassword.handler").verifyPasswordHandler;
|
||||
verifyCodeUnAuthenticated?: typeof import("./verifyCodeUnAuthenticated.handler").verifyCodeUnAuthenticatedHandler;
|
||||
resendVerifyEmail?: typeof import("./resendVerifyEmail.handler").resendVerifyEmail;
|
||||
sendVerifyEmailCode?: typeof import("./sendVerifyEmailCode.handler").sendVerifyEmailCodeHandler;
|
||||
};
|
||||
|
||||
const UNSTABLE_HANDLER_CACHE: AuthRouterHandlerCache = {};
|
||||
|
@ -47,6 +53,41 @@ export const authRouter = router({
|
|||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
verifyCodeUnAuthenticated: publicProcedure.input(ZVerifyCodeInputSchema).mutation(async ({ input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.verifyCodeUnAuthenticated) {
|
||||
UNSTABLE_HANDLER_CACHE.verifyCodeUnAuthenticated = await import(
|
||||
"./verifyCodeUnAuthenticated.handler"
|
||||
).then((mod) => mod.verifyCodeUnAuthenticatedHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.verifyCodeUnAuthenticated) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.verifyCodeUnAuthenticated({
|
||||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
sendVerifyEmailCode: publicProcedure.input(ZSendVerifyEmailCodeSchema).mutation(async ({ input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.sendVerifyEmailCode) {
|
||||
UNSTABLE_HANDLER_CACHE.sendVerifyEmailCode = await import("./sendVerifyEmailCode.handler").then(
|
||||
(mod) => mod.sendVerifyEmailCodeHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.sendVerifyEmailCode) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.sendVerifyEmailCode({
|
||||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
resendVerifyEmail: authedProcedure.mutation(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.resendVerifyEmail) {
|
||||
UNSTABLE_HANDLER_CACHE.resendVerifyEmail = await import("./resendVerifyEmail.handler").then(
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
import { TSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema"
|
||||
|
||||
type SendVerifyEmailCode = {
|
||||
input: TSendVerifyEmailCodeSchema;
|
||||
};
|
||||
|
||||
const log = logger.getChildLogger({ prefix: [`[[Auth] `] });
|
||||
|
||||
export const sendVerifyEmailCodeHandler = async ({ input }: SendVerifyEmailCode) => {
|
||||
const email = await sendEmailVerificationByCode({
|
||||
email: input.email,
|
||||
username: input.username,
|
||||
language: input.language,
|
||||
});
|
||||
|
||||
return email;
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZSendVerifyEmailCodeSchema = z.object({
|
||||
email: z.string().min(1),
|
||||
username: z.string().optional(),
|
||||
language: z.string().optional()
|
||||
});
|
||||
|
||||
export type TSendVerifyEmailCodeSchema = z.infer<typeof ZSendVerifyEmailCodeSchema>;
|
|
@ -0,0 +1,27 @@
|
|||
import { createHash } from "crypto";
|
||||
import { totp } from "otplib";
|
||||
|
||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
type VerifyTokenOptions = {
|
||||
input: ZVerifyCodeInputSchema;
|
||||
};
|
||||
|
||||
export const verifyCodeUnAuthenticatedHandler = async ({ input }: VerifyTokenOptions) => {
|
||||
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");
|
||||
|
||||
totp.options = { step: 900 };
|
||||
const isValidToken = totp.check(code, secret);
|
||||
|
||||
if (!isValidToken) throw new TRPCError({ code: "BAD_REQUEST", message: "invalid_code" });
|
||||
|
||||
return isValidToken;
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import authedProcedure, { authedAdminProcedure } from "../../../procedures/authedProcedure";
|
||||
import { router } from "../../../trpc";
|
||||
import { ZAdminVerifyInput } from "./adminVerify.schema";
|
||||
|
@ -7,7 +9,6 @@ import { ZGetMembersInput } from "./getMembers.schema";
|
|||
import { ZListMembersSchema } from "./listMembers.schema";
|
||||
import { ZSetPasswordSchema } from "./setPassword.schema";
|
||||
import { ZUpdateInputSchema } from "./update.schema";
|
||||
import { ZVerifyCodeInputSchema } from "./verifyCode.schema";
|
||||
|
||||
type OrganizationsRouterHandlerCache = {
|
||||
create?: typeof import("./create.handler").createHandler;
|
||||
|
|
|
@ -2,12 +2,12 @@ import { createHash } from "crypto";
|
|||
import { totp } from "otplib";
|
||||
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { ZVerifyCodeInputSchema } from "./verifyCode.schema";
|
||||
|
||||
type VerifyCodeOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
|
@ -22,6 +22,10 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => {
|
|||
if (!user || !email || !code) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
if (!IS_PRODUCTION) return true;
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: email,
|
||||
});
|
||||
|
||||
const secret = createHash("md5")
|
||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZVerifyCodeInputSchema = z.object({
|
||||
email: z.string().email(),
|
||||
code: z.string(),
|
||||
});
|
||||
|
||||
export type ZVerifyCodeInputSchema = z.infer<typeof ZVerifyCodeInputSchema>;
|
Loading…
Reference in New Issue
Block a user