Skip to content

Commit c196ff1

Browse files
Udit-takkarvolnei
andauthored
feat: link email to participant (requireEmailForGuests) (calcom#24661)
* feat: link email to participatn * fix: bugs * refactor: improve code * refactor: prevent repload * chore: remove unued * fix: type * refactor * fix: type * feat: restrict host * feat: type * feat: tests * fix: don't allow guest * fix: merk guest * fix: bugs * fix: test * fix: test * refactor: feedback * fix: tests --------- Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
1 parent bf0fbe4 commit c196ff1

28 files changed

Lines changed: 618 additions & 155 deletions

File tree

apps/web/app/api/cancel/route.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { NextRequest } from "next/server";
66
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
77
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
88
import { bookingCancelWithCsrfSchema } from "@calcom/prisma/zod-utils";
9+
import { validateCsrfToken } from "@calcom/web/lib/validateCsrfToken";
910

1011
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
1112

@@ -17,13 +18,11 @@ async function handler(req: NextRequest) {
1718
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
1819
}
1920
const bookingData = bookingCancelWithCsrfSchema.parse(appDirRequestBody);
20-
const cookieStore = await cookies();
21-
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
2221

23-
if (!cookieToken || cookieToken !== bookingData.csrfToken) {
24-
return NextResponse.json({ success: false, message: "Invalid CSRF token" }, { status: 403 });
22+
const csrfError = await validateCsrfToken(bookingData.csrfToken);
23+
if (csrfError) {
24+
return csrfError;
2525
}
26-
cookieStore.delete("calcom.csrf_token");
2726

2827
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
2928

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
2+
import { NextResponse } from "next/server";
3+
import type { NextRequest } from "next/server";
4+
import { z } from "zod";
5+
6+
import {
7+
generateGuestMeetingTokenFromOwnerMeetingToken,
8+
updateMeetingTokenIfExpired,
9+
} from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
10+
import { getHostsAndGuests } from "@calcom/features/bookings/lib/getHostsAndGuests";
11+
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
12+
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
13+
import { VideoCallGuestRepository } from "@calcom/features/video-call-guest/repositories/VideoCallGuestRepository";
14+
import prisma from "@calcom/prisma";
15+
import { validateCsrfToken } from "@calcom/web/lib/validateCsrfToken";
16+
17+
const videoCallGuestWithCsrfSchema = z.object({
18+
bookingUid: z.string(),
19+
email: z.string().email(),
20+
name: z.string().min(1),
21+
csrfToken: z.string().length(64, "Invalid CSRF token"),
22+
});
23+
24+
async function handler(req: NextRequest) {
25+
let appDirRequestBody;
26+
try {
27+
appDirRequestBody = await req.json();
28+
} catch {
29+
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
30+
}
31+
32+
const guestData = videoCallGuestWithCsrfSchema.parse(appDirRequestBody);
33+
34+
const csrfError = await validateCsrfToken(guestData.csrfToken);
35+
if (csrfError) {
36+
return csrfError;
37+
}
38+
39+
const bookingRepo = new BookingRepository(prisma);
40+
const booking = await bookingRepo.findBookingIncludeCalVideoSettingsAndReferences({
41+
bookingUid: guestData.bookingUid,
42+
});
43+
44+
if (!booking) {
45+
return NextResponse.json({ error: "Booking not found" }, { status: 404 });
46+
}
47+
48+
const { hosts, guests } = getHostsAndGuests(booking);
49+
const isHost = hosts.some((host) => host.email.toLowerCase() === guestData.email.trim().toLowerCase());
50+
const isGuest = guests.some((guest) => guest.email.toLowerCase() === guestData.email.trim().toLowerCase());
51+
52+
if (isHost) {
53+
return NextResponse.json({ error: "hosts_must_use_login" }, { status: 403 });
54+
} else if (!isGuest) {
55+
return NextResponse.json({ error: "invalid_guest_email" }, { status: 403 });
56+
}
57+
58+
const videoCallGuestRepo = new VideoCallGuestRepository(prisma);
59+
const guestSession = await videoCallGuestRepo.upsertVideoCallGuest({
60+
bookingUid: guestData.bookingUid,
61+
email: guestData.email,
62+
name: guestData.name,
63+
});
64+
65+
const videoReference = getCalVideoReference(booking.references);
66+
67+
if (!videoReference) {
68+
return NextResponse.json({ error: "Video reference not found" }, { status: 404 });
69+
}
70+
71+
const endTime = new Date(booking.endTime);
72+
const fourteenDaysAfter = new Date(endTime.getTime() + 14 * 24 * 60 * 60 * 1000);
73+
const epochTimeFourteenDaysAfter = Math.floor(fourteenDaysAfter.getTime() / 1000);
74+
75+
const videoReferencePassword = await updateMeetingTokenIfExpired({
76+
bookingReferenceId: videoReference.id,
77+
roomName: videoReference.uid,
78+
meetingToken: videoReference.meetingPassword,
79+
exp: epochTimeFourteenDaysAfter,
80+
});
81+
82+
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken({
83+
meetingToken: videoReferencePassword,
84+
userId: guestSession.id,
85+
});
86+
87+
return NextResponse.json({
88+
guestSessionId: guestSession.id,
89+
meetingPassword: guestMeetingPassword,
90+
meetingUrl: videoReference.meetingUrl,
91+
});
92+
}
93+
94+
export const POST = defaultResponderForAppDir(handler);

apps/web/lib/validateCsrfToken.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { cookies } from "next/headers";
2+
import { NextResponse } from "next/server";
3+
4+
export async function validateCsrfToken(csrfToken: string): Promise<NextResponse | null> {
5+
const cookieStore = await cookies();
6+
const cookieToken = cookieStore.get("calcom.csrf_token")?.value;
7+
8+
if (!cookieToken || cookieToken !== csrfToken) {
9+
return NextResponse.json({ success: false, message: "Invalid CSRF token" }, { status: 403 });
10+
}
11+
cookieStore.delete("calcom.csrf_token");
12+
return null;
13+
}

apps/web/lib/video/[uid]/getServerSideProps.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
1010
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
1111
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
1212
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
13-
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
1413
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
1514
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
1615
import { CAL_VIDEO_MEETING_LINK_FOR_TESTING } from "@calcom/lib/constants";
@@ -26,6 +25,7 @@ type CalVideoSettings = {
2625
enableAutomaticRecordingForOrganizer: boolean;
2726
disableTranscriptionForGuests: boolean;
2827
disableTranscriptionForOrganizer: boolean;
28+
requireEmailForGuests: boolean;
2929
};
3030

3131
const shouldEnableRecordButton = ({
@@ -124,7 +124,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
124124
const { req } = context;
125125

126126
const bookingRepo = new BookingRepository(prisma);
127-
const booking = await bookingRepo.findBookingForMeetingPage({
127+
const booking = await bookingRepo.findBookingIncludeCalVideoSettingsAndReferences({
128128
bookingUid: context.query.uid as string,
129129
});
130130

@@ -211,6 +211,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
211211
});
212212

213213
const sessionUserId = session?.user?.impersonatedBy ? session.user.impersonatedBy.id : session?.user.id;
214+
const sessionUserEmail = session?.user?.email;
214215
const isOrganizer = await checkIfUserIsHost({
215216
booking: {
216217
eventTypeId: bookingObj.eventType?.id,
@@ -219,18 +220,25 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
219220
sessionUserId,
220221
});
221222

223+
const isAttendee = sessionUserEmail
224+
? bookingObj.attendees?.some(
225+
(attendee) => attendee.email.toLowerCase() === sessionUserEmail.toLowerCase()
226+
) ?? false
227+
: false;
228+
222229
// set meetingPassword for guests
223230
if (!isOrganizer) {
231+
const userIdForToken = sessionUserId;
232+
224233
const guestMeetingPassword = await generateGuestMeetingTokenFromOwnerMeetingToken({
225234
meetingToken: videoReferencePassword,
226-
userId: sessionUserId,
235+
userId: userIdForToken,
227236
});
228237

229238
bookingObj.references.forEach((bookRef) => {
230239
bookRef.meetingPassword = guestMeetingPassword;
231240
});
232241
}
233-
234242
// Only for backward compatibility and setting user id in participants for organizer
235243
else {
236244
const meetingPassword = await setEnableRecordingUIAndUserIdForOrganizer(
@@ -247,11 +255,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
247255

248256
const videoReference = getCalVideoReference(bookingObj.references);
249257

250-
const featureRepo = new FeaturesRepository(prisma);
251-
const displayLogInOverlay = profile?.organizationId
252-
? await featureRepo.checkIfTeamHasFeature(profile.organizationId, "cal-video-log-in-overlay")
253-
: false;
254-
255258
const showRecordingButton = shouldEnableRecordButton({
256259
hasTeamPlan: !!hasTeamPlan,
257260
calVideoSettings: bookingObj.eventType?.calVideoSettings,
@@ -293,7 +296,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
293296
},
294297
hasTeamPlan: !!hasTeamPlan,
295298
calVideoLogo,
296-
displayLogInOverlay,
297299
loggedInUserName: sessionUserId ? session?.user?.name : undefined,
298300
showRecordingButton,
299301
enableAutomaticTranscription,
@@ -303,6 +305,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
303305
? undefined
304306
: bookingObj.eventType?.calVideoSettings?.redirectUrlOnExit,
305307
overrideName: Array.isArray(context.query.name) ? context.query.name[0] : context.query.name,
308+
requireEmailForGuests: bookingObj.eventType?.calVideoSettings?.requireEmailForGuests ?? false,
309+
isLoggedInUserPartOfMeeting: isAttendee || isOrganizer,
306310
},
307311
};
308312
}

0 commit comments

Comments
 (0)