Skip to content

Commit

Permalink
Merge branch 'main' into feat/add-booking-and-user-creation-source
Browse files Browse the repository at this point in the history
  • Loading branch information
alishaz-polymath authored Jan 21, 2025
2 parents 8444bfc + 30eb7ce commit dd1ad20
Show file tree
Hide file tree
Showing 18 changed files with 88 additions and 9 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ E2E_TEST_MAILHOG_ENABLED=

# Cloudflare Turnstile
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
CLOUDFLARE_TURNSTILE_SECRET=

# Set the following value to true if you wish to enable Team Impersonation
Expand Down
2 changes: 1 addition & 1 deletion apps/web/modules/signup-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const signupSchema = apiSignupSchema.extend({
cfToken: z.string().optional(),
});

const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false });
const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false });

type FormValues = z.infer<typeof signupSchema>;

Expand Down
8 changes: 8 additions & 0 deletions apps/web/pages/api/book/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
import getIP from "@calcom/lib/getIP";
import { defaultResponder } from "@calcom/lib/server";
import { CreationSource } from "@calcom/prisma/enums";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";

async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const userIp = getIP(req);

if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") {
await checkCfTurnstileToken({
token: req.body["cfToken"] as string,
remoteIp: userIp,
});
}

await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: userIp,
Expand Down
8 changes: 8 additions & 0 deletions apps/web/pages/api/book/recurring-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ import type { BookingResponse } from "@calcom/features/bookings/types";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import getIP from "@calcom/lib/getIP";
import { defaultResponder } from "@calcom/lib/server";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";

// @TODO: Didn't look at the contents of this function in order to not break old booking page.

async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const userIp = getIP(req);

if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") {
await checkCfTurnstileToken({
token: req.body[0]["cfToken"] as string,
remoteIp: userIp,
});
}

await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: userIp,
Expand Down
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,7 @@
"download_transcript": "Download Transcript",
"recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download",
"transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour",
"you_can_download_transcript_from_attachments": "You can also download transcript from attachments",
"link_valid_for_12_hrs": "Note: The download link is valid only for 12 hours. You can generate new download link by following instructions <1>here</1>.",
"create_your_first_form": "Create your first form",
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { TFunction } from "next-i18next";

import { WEBAPP_URL, APP_NAME, COMPANY_NAME } from "@calcom/lib/constants";
import { WEBAPP_URL, COMPANY_NAME } from "@calcom/lib/constants";

import { V2BaseEmailHtml, CallToAction } from "../components";
import { V2BaseEmailHtml } from "../components";

interface DailyVideoDownloadTranscriptEmailProps {
language: TFunction;
Expand Down Expand Up @@ -55,13 +55,13 @@ export const DailyVideoDownloadTranscriptEmail = (
<>{props.language("hi_user_name", { name: props.name })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "40px" }}>
<>{props.language("transcript_from_previous_call", { appName: APP_NAME })}</>
<>{props.language("you_can_download_transcript_from_attachments")}</>
</p>

{props.transcriptDownloadLinks.map((downloadLink, index) => {
{props.transcriptDownloadLinks.map((_, index) => {
return (
<div
key={downloadLink}
key={`transcript-${index}`}
style={{
backgroundColor: "#F3F4F6",
padding: "32px",
Expand All @@ -87,7 +87,6 @@ export const DailyVideoDownloadTranscriptEmail = (
}}>
{props.date} Transcript {index + 1}
</p>
<CallToAction label={props.language("download_transcript")} href={downloadLink} />
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail
this.t = attendee.language.translate;
}
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const attachments = await Promise.all(
this.transcriptDownloadLinks.map(async (url, index) => {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
return {
filename: `transcript-${index + 1}.vtt`,
content: Buffer.from(buffer),
contentType: "text/vtt",
};
})
);

return {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
Expand All @@ -36,6 +48,7 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail
language: this.t,
name: this.attendee.name,
}),
attachments,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai
this.t = this.calEvent.organizer.language.translate;
}
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const attachments = await Promise.all(
this.transcriptDownloadLinks.map(async (url, index) => {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
return {
filename: `transcript-${index + 1}.vtt`,
content: Buffer.from(buffer),
contentType: "text/vtt",
};
})
);

return {
to: `${this.calEvent.organizer.email}>`,
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
Expand All @@ -35,6 +47,7 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai
language: this.t,
name: this.calEvent.organizer.name,
}),
attachments,
};
}

Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions packages/features/bookings/Booker/Booker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const BookerComponent = ({
areInstantMeetingParametersSet = false,
userLocale,
hasValidLicense,
renderCaptcha,
}: BookerProps & WrappedBookerProps) => {
const searchParams = useCompatSearchParams();
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
Expand Down Expand Up @@ -166,6 +167,7 @@ const BookerComponent = ({
return bookerState === "booking" ? (
<BookEventForm
key={key}
renderCaptcha={renderCaptcha}
onCancel={() => {
setSelectedTimeslot(null);
if (seatedEventData.bookingUid) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useMemo, useState } from "react";
import type { FieldError } from "react-hook-form";

import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
import {
WEBSITE_PRIVACY_POLICY_URL,
WEBSITE_TERMS_URL,
CLOUDFLARE_SITE_ID,
CLOUDFLARE_USE_TURNSTILE_IN_BOOKER,
} from "@calcom/lib/constants";
import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, EmptyScreen, Form } from "@calcom/ui";
Expand All @@ -17,6 +23,8 @@ import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBo
import { BookingFields } from "./BookingFields";
import { FormSkeleton } from "./Skeleton";

const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false });

type BookEventFormProps = {
onCancel?: () => void;
onSubmit: () => void;
Expand All @@ -29,6 +37,7 @@ type BookEventFormProps = {
extraOptions: Record<string, string | string[]>;
isPlatform?: boolean;
isVerificationCodeSending: boolean;
renderCaptcha?: boolean;
};

export const BookEventForm = ({
Expand All @@ -45,6 +54,7 @@ export const BookEventForm = ({
extraOptions,
isVerificationCodeSending,
isPlatform = false,
renderCaptcha,
}: Omit<BookEventFormProps, "event"> & {
eventQuery: {
isError: boolean;
Expand All @@ -61,6 +71,10 @@ export const BookEventForm = ({
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();

// Cloudflare Turnstile Captcha
const shouldRenderCaptcha =
renderCaptcha && CLOUDFLARE_SITE_ID && CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1";

const [responseVercelIdHeader] = useState<string | null>(null);
const { t } = useLocale();

Expand Down Expand Up @@ -88,6 +102,8 @@ export const BookEventForm = ({
return <Alert severity="warning" message={t("error_booking_event")} />;
}

const watchedCfToken = bookingForm.watch("cfToken");

return (
<div className="flex h-full flex-col">
<Form
Expand Down Expand Up @@ -120,6 +136,15 @@ export const BookEventForm = ({
/>
</div>
)}
{/* Cloudflare Turnstile Captcha */}
{shouldRenderCaptcha ? (
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
bookingForm.setValue("cfToken", token);
}}
/>
) : null}
{!isPlatform && (
<div className="text-subtle my-3 w-full text-xs">
<Trans
Expand All @@ -143,6 +168,7 @@ export const BookEventForm = ({
/>
</div>
)}

{isPlatformBookerEmbed && (
<div className="text-subtle my-3 w-full text-xs">
{t("proceeding_agreement")}{" "}
Expand Down Expand Up @@ -176,9 +202,11 @@ export const BookEventForm = ({
{t("back")}
</Button>
)}

<Button
type="submit"
color="primary"
disabled={!!shouldRenderCaptcha && !watchedCfToken}
loading={
loadingStates.creatingBooking ||
loadingStates.creatingRecurringBooking ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const useBookingForm = ({
// Key is not really part of form values, but only used to have a key
// to set generic error messages on. Needed until RHF has implemented root error keys.
globalError: undefined;
cfToken?: string;
};
const isRescheduling = !!rescheduleUid && !!bookingData;

Expand Down
1 change: 1 addition & 0 deletions packages/features/bookings/Booker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type WrappedBookerPropsMain = {
bookerLayout: UseBookerLayoutType;
verifyEmail: UseVerifyEmailReturnType;
customClassNames?: CustomClassNames;
renderCaptcha?: boolean;
};

export type WrappedBookerPropsForPlatform = WrappedBookerPropsMain & {
Expand Down
1 change: 1 addition & 0 deletions packages/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const BOOKER_NUMBER_OF_DAYS_TO_LOAD = parseInt(
);

export const CLOUDFLARE_SITE_ID = process.env.NEXT_PUBLIC_CLOUDFLARE_SITEKEY;
export const CLOUDFLARE_USE_TURNSTILE_IN_BOOKER = process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER;
export const MINIMUM_NUMBER_OF_ORG_SEATS = 30;
export const ORG_SELF_SERVE_ENABLED = process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED === "1";
export const ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE = 0;
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/server/checkCfTurnstileToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HttpError } from "../http-error";
const TURNSTILE_SECRET_ID = process.env.CLOUDFLARE_TURNSTILE_SECRET;

export async function checkCfTurnstileToken({ token, remoteIp }: { token?: string; remoteIp: string }) {
// This means the instant doesnt have turnstile enabled - we skip the check and just return success.
// This means the instance doesnt have turnstile enabled - we skip the check and just return success.
// OR the instance is running in CI so we skip these checks also
if (!TURNSTILE_SECRET_ID || !!process.env.NEXT_PUBLIC_IS_E2E) {
return {
Expand Down
1 change: 1 addition & 0 deletions packages/platform/atoms/booker/BookerWebWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
isPlatform={false}
areInstantMeetingParametersSet={areInstantMeetingParametersSet}
userLocale={session?.user.locale}
renderCaptcha
/>
);
};
1 change: 1 addition & 0 deletions packages/prisma/zod-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export const bookingCreateBodySchema = z.object({
routingFormResponseId: z.number().optional(),
skipContactOwner: z.boolean().optional(),
crmAppSlug: z.string().nullish().optional(),
cfToken: z.string().nullish().optional(),

/**
* Holds the corrected responses of the Form for a booking, provided during rerouting
Expand Down
1 change: 1 addition & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@
"NEXT_PUBLIC_TEAM_IMPERSONATION",
"NEXT_PUBLIC_VERCEL_URL",
"NEXT_PUBLIC_CLOUDFLARE_SITEKEY",
"NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER",
"NEXT_PUBLIC_VERCEL_ENV",
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
"NEXT_PUBLIC_GTM_ID",
Expand Down

0 comments on commit dd1ad20

Please sign in to comment.