-
-
-
-
-
-
+const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => {
+ console.log(profile.slug);
+ return (
+
-
-);
+ );
+};
const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypeProps) => {
const { t } = useLocale();
diff --git a/apps/web/pages/team/[slug]/book.tsx b/apps/web/pages/team/[slug]/book.tsx
index faa0831132..24c46d5464 100644
--- a/apps/web/pages/team/[slug]/book.tsx
+++ b/apps/web/pages/team/[slug]/book.tsx
@@ -98,6 +98,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
eventType: eventTypeObject,
booking,
isDynamicGroupBooking: false,
+ hasHashedBookingLink: false,
+ hashedLink: null,
},
};
}
diff --git a/apps/web/playwright/event-types.test.ts b/apps/web/playwright/event-types.test.ts
index 06a4ab2a82..f277a3cab1 100644
--- a/apps/web/playwright/event-types.test.ts
+++ b/apps/web/playwright/event-types.test.ts
@@ -69,6 +69,25 @@ test.describe("Event Types tests", () => {
await expect(formTitle).toBe(firstTitle);
await expect(formSlug).toBe(firstSlug);
});
+ test("edit first event", async ({ page }) => {
+ const $eventTypes = await page.locator("[data-testid=event-types] > *");
+ const firstEventTypeElement = await $eventTypes.first();
+ await firstEventTypeElement.click();
+ await page.waitForNavigation({
+ url: (url) => {
+ return !!url.pathname.match(/\/event-types\/.+/);
+ },
+ });
+ await expect(page.locator("[data-testid=advanced-settings-content]")).not.toBeVisible();
+ await page.locator("[data-testid=show-advanced-settings]").click();
+ await expect(page.locator("[data-testid=advanced-settings-content]")).toBeVisible();
+ await page.locator("[data-testid=update-eventtype]").click();
+ await page.waitForNavigation({
+ url: (url) => {
+ return url.pathname.endsWith("/event-types");
+ },
+ });
+ });
});
test.describe("free user", () => {
@@ -88,5 +107,25 @@ test.describe("Event Types tests", () => {
test("can not add new event type", async ({ page }) => {
await expect(page.locator("[data-testid=new-event-type]")).toBeDisabled();
});
+
+ test("edit first event", async ({ page }) => {
+ const $eventTypes = await page.locator("[data-testid=event-types] > *");
+ const firstEventTypeElement = await $eventTypes.first();
+ await firstEventTypeElement.click();
+ await page.waitForNavigation({
+ url: (url) => {
+ return !!url.pathname.match(/\/event-types\/.+/);
+ },
+ });
+ await expect(page.locator("[data-testid=advanced-settings-content]")).not.toBeVisible();
+ await page.locator("[data-testid=show-advanced-settings]").click();
+ await expect(page.locator("[data-testid=advanced-settings-content]")).toBeVisible();
+ await page.locator("[data-testid=update-eventtype]").click();
+ await page.waitForNavigation({
+ url: (url) => {
+ return url.pathname.endsWith("/event-types");
+ },
+ });
+ });
});
});
diff --git a/apps/web/playwright/hash-my-url.test.ts b/apps/web/playwright/hash-my-url.test.ts
new file mode 100644
index 0000000000..74bb136d88
--- /dev/null
+++ b/apps/web/playwright/hash-my-url.test.ts
@@ -0,0 +1,75 @@
+import { expect, test } from "@playwright/test";
+
+import { deleteAllBookingsByEmail } from "./lib/teardown";
+import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
+
+test.describe("hash my url", () => {
+ test.use({ storageState: "playwright/artifacts/proStorageState.json" });
+ let $url = "";
+ test.beforeEach(async ({ page }) => {
+ await deleteAllBookingsByEmail("pro@example.com");
+ await page.goto("/event-types");
+ // We wait until loading is finished
+ await page.waitForSelector('[data-testid="event-types"]');
+ });
+
+ test.afterAll(async () => {
+ // delete test bookings
+ await deleteAllBookingsByEmail("pro@example.com");
+ });
+
+ test("generate url hash", async ({ page }) => {
+ // await page.pause();
+ await page.goto("/event-types");
+ // We wait until loading is finished
+ await page.waitForSelector('[data-testid="event-types"]');
+ await page.click('//ul[@data-testid="event-types"]/li[1]');
+ // We wait for the page to load
+ await page.waitForSelector('//*[@data-testid="show-advanced-settings"]');
+ await page.click('//*[@data-testid="show-advanced-settings"]');
+ // we wait for the hashedLink setting to load
+ await page.waitForSelector('//*[@id="hashedLink"]');
+ await page.click('//*[@id="hashedLink"]');
+ // click update
+ await page.focus('//button[@type="submit"]');
+ await page.keyboard.press("Enter");
+ });
+
+ test("book using generated url hash", async ({ page }) => {
+ // await page.pause();
+ await page.goto("/event-types");
+ // We wait until loading is finished
+ await page.waitForSelector('[data-testid="event-types"]');
+ await page.click('//ul[@data-testid="event-types"]/li[1]');
+ // We wait for the page to load
+ await page.waitForSelector('//*[@data-testid="show-advanced-settings"]');
+ await page.click('//*[@data-testid="show-advanced-settings"]');
+ // we wait for the hashedLink setting to load
+ await page.waitForSelector('//*[@data-testid="generated-hash-url"]');
+ $url = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
+ await page.goto($url);
+ await selectFirstAvailableTimeSlotNextMonth(page);
+ await bookTimeSlot(page);
+
+ // Make sure we're navigated to the success page
+ await page.waitForNavigation({
+ url(url) {
+ return url.pathname.endsWith("/success");
+ },
+ });
+ });
+
+ test("hash regenerates after successful booking", async ({ page }) => {
+ await page.goto("/event-types");
+ // We wait until loading is finished
+ await page.waitForSelector('[data-testid="event-types"]');
+ await page.click('//ul[@data-testid="event-types"]/li[1]');
+ // We wait for the page to load
+ await page.waitForSelector('//*[@data-testid="show-advanced-settings"]');
+ await page.click('//*[@data-testid="show-advanced-settings"]');
+ // we wait for the hashedLink setting to load
+ await page.waitForSelector('//*[@data-testid="generated-hash-url"]');
+ const $newUrl = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
+ expect($url !== $newUrl).toBeTruthy();
+ });
+});
diff --git a/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt b/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt
index 2d8e6d9203..07fe93f9cc 100644
--- a/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt
+++ b/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt
@@ -1 +1 @@
-{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30min","title":"30min between Pro Example and Test Testson","description":null,"additionalNotes":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Pro Example","email":"pro@example.com","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}
\ No newline at end of file
+{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30min","title":"30min between Pro Example and Test Testson","description":"","additionalNotes":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Pro Example","email":"pro@example.com","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}
\ No newline at end of file
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 68bf5b56a6..9f0ba50c35 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -500,6 +500,7 @@
"url": "URL",
"hidden": "Hidden",
"readonly": "Readonly",
+ "one_time_link": "One-time link",
"plan_description": "You're currently on the {{plan}} plan.",
"plan_upgrade_invitation": "Upgrade your account to the pro plan to unlock all of the features we have to offer.",
"plan_upgrade": "You need to upgrade your plan to have more than one active event type.",
@@ -583,6 +584,8 @@
"opt_in_booking_description": "The booking needs to be manually confirmed before it is pushed to the integrations and a confirmation mail is sent.",
"disable_guests": "Disable Guests",
"disable_guests_description": "Disable adding additional guests while booking.",
+ "hashed_link": "Generate hashed URL",
+ "hashed_link_description": "Generate a hashed URL to share without exposing your Cal username",
"invitees_can_schedule": "Invitees can schedule",
"date_range": "Date Range",
"calendar_days": "calendar days",
@@ -745,6 +748,7 @@
"success_api_key_created_bold_tagline": "Save this API key somewhere safe.",
"you_will_only_view_it_once": "You will not be able to view it again once you close this modal.",
"copy_to_clipboard": "Copy to clipboard",
+ "enabled_after_update": "Enabled after update",
"confirm_delete_api_key": "Revoke this API key",
"revoke_api_key": "Revoke API key",
"api_key_copied": "API key copied!",
@@ -778,5 +782,6 @@
"test_your_trigger": "4. Test your Trigger.",
"you_are_set": "5. You're set!",
"install_zapier_app": "Please first install the Zapier App in the app store.",
- "go_to_app_store": "Go to App Store"
+ "go_to_app_store": "Go to App Store",
+ "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions"
}
diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx
index 7719da5d90..2894d3ce27 100644
--- a/apps/web/server/routers/viewer.tsx
+++ b/apps/web/server/routers/viewer.tsx
@@ -133,11 +133,11 @@ const loggedInViewerRouter = createProtectedRouter()
currency: true,
position: true,
successRedirectUrl: true,
+ hashedLink: true,
users: {
select: {
id: true,
username: true,
- avatar: true,
name: true,
},
},
@@ -154,7 +154,6 @@ const loggedInViewerRouter = createProtectedRouter()
startTime: true,
endTime: true,
bufferTime: true,
- avatar: true,
plan: true,
teams: {
where: {
@@ -230,7 +229,6 @@ const loggedInViewerRouter = createProtectedRouter()
profile: {
slug: typeof user["username"];
name: typeof user["name"];
- image: typeof user["avatar"];
};
metadata: {
membershipCount: number;
@@ -255,7 +253,6 @@ const loggedInViewerRouter = createProtectedRouter()
profile: {
slug: user.username,
name: user.name,
- image: user.avatar,
},
eventTypes: _.orderBy(mergedEventTypes, ["position", "id"], ["desc", "asc"]),
metadata: {
diff --git a/apps/web/server/routers/viewer/eventTypes.tsx b/apps/web/server/routers/viewer/eventTypes.tsx
index 48c3d04de4..dd165ced6a 100644
--- a/apps/web/server/routers/viewer/eventTypes.tsx
+++ b/apps/web/server/routers/viewer/eventTypes.tsx
@@ -1,4 +1,6 @@
import { EventTypeCustomInput, MembershipRole, PeriodType, Prisma } from "@prisma/client";
+import short from "short-uuid";
+import { v5 as uuidv5 } from "uuid";
import { z } from "zod";
import {
@@ -89,6 +91,7 @@ const EventTypeUpdateInput = _EventTypeModel
}),
users: z.array(stringOrNumber).optional(),
schedule: z.number().optional(),
+ hashedLink: z.boolean(),
})
.partial()
.merge(
@@ -117,6 +120,7 @@ export const eventTypesRouter = createProtectedRouter()
const data: Prisma.EventTypeCreateInput = {
...rest,
+ userId: teamId ? undefined : userId,
users: {
connect: {
id: userId,
@@ -213,8 +217,17 @@ export const eventTypesRouter = createProtectedRouter()
.mutation("update", {
input: EventTypeUpdateInput.strict(),
async resolve({ ctx, input }) {
- const { schedule, periodType, locations, destinationCalendar, customInputs, users, id, ...rest } =
- input;
+ const {
+ schedule,
+ periodType,
+ locations,
+ destinationCalendar,
+ customInputs,
+ users,
+ id,
+ hashedLink,
+ ...rest
+ } = input;
assertValidUrl(input.successRedirectUrl);
const data: Prisma.EventTypeUpdateInput = rest;
data.locations = locations ?? undefined;
@@ -249,6 +262,48 @@ export const eventTypesRouter = createProtectedRouter()
};
}
+ const connectedLink = await ctx.prisma.hashedLink.findFirst({
+ where: {
+ eventTypeId: input.id,
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ if (hashedLink) {
+ // check if hashed connection existed. If it did, do nothing. If it didn't, add a new connection
+ if (!connectedLink) {
+ const translator = short();
+ const seed = `${input.eventName}:${input.id}:${new Date().getTime()}`;
+ const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
+ // create a hashed link
+ await ctx.prisma.hashedLink.upsert({
+ where: {
+ eventTypeId: input.id,
+ },
+ update: {
+ link: uid,
+ },
+ create: {
+ link: uid,
+ eventType: {
+ connect: { id: input.id },
+ },
+ },
+ });
+ }
+ } else {
+ // check if hashed connection exists. If it does, disconnect
+ if (connectedLink) {
+ await ctx.prisma.hashedLink.delete({
+ where: {
+ eventTypeId: input.id,
+ },
+ });
+ }
+ }
+
const eventType = await ctx.prisma.eventType.update({
where: { id },
data,
diff --git a/apps/website b/apps/website
index ac4ce5571f..300d090ebe 160000
--- a/apps/website
+++ b/apps/website
@@ -1 +1 @@
-Subproject commit ac4ce5571f91b49c05cb19e71f8c58b5d3f6d131
+Subproject commit 300d090ebe5772b2b22432931ba1a837b4e5e759
diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts
index c5e1a69c7f..ff9863f91b 100644
--- a/packages/app-store/googlecalendar/api/callback.ts
+++ b/packages/app-store/googlecalendar/api/callback.ts
@@ -2,6 +2,7 @@ import { google } from "googleapis";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
+import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
@@ -10,7 +11,6 @@ const credentials = process.env.GOOGLE_API_CREDENTIALS;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
-
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
@@ -19,7 +19,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.status(400).json({ message: "There are no Google Credentials installed." });
return;
}
-
const { client_secret, client_id } = JSON.parse(credentials).web;
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
@@ -41,5 +40,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
const state = decodeOAuthState(req);
- res.redirect(state?.returnTo ?? "/apps/installed");
+ res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed");
}
diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts
index 1e742aa35c..20fd3acc7a 100644
--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -72,21 +72,21 @@ export default class GoogleCalendarService implements Calendar {
};
};
- async createEvent(event: CalendarEvent): Promise
{
+ async createEvent(calEventRaw: CalendarEvent): Promise {
return new Promise((resolve, reject) =>
this.auth.getToken().then((myGoogleAuth) => {
const payload: calendar_v3.Schema$Event = {
- summary: event.title,
- description: getRichDescription(event),
+ summary: calEventRaw.title,
+ description: getRichDescription(calEventRaw),
start: {
- dateTime: event.startTime,
- timeZone: event.organizer.timeZone,
+ dateTime: calEventRaw.startTime,
+ timeZone: calEventRaw.organizer.timeZone,
},
end: {
- dateTime: event.endTime,
- timeZone: event.organizer.timeZone,
+ dateTime: calEventRaw.endTime,
+ timeZone: calEventRaw.organizer.timeZone,
},
- attendees: event.attendees.map((attendee) => ({
+ attendees: calEventRaw.attendees.map((attendee) => ({
...attendee,
responseStatus: "accepted",
})),
@@ -95,23 +95,21 @@ export default class GoogleCalendarService implements Calendar {
},
};
- if (event.location) {
- payload["location"] = getLocation(event);
+ if (calEventRaw.location) {
+ payload["location"] = getLocation(calEventRaw);
}
- if (event.conferenceData && event.location === "integrations:google:meet") {
- payload["conferenceData"] = event.conferenceData;
+ if (calEventRaw.conferenceData && calEventRaw.location === "integrations:google:meet") {
+ payload["conferenceData"] = calEventRaw.conferenceData;
}
-
const calendar = google.calendar({
version: "v3",
- auth: myGoogleAuth,
});
calendar.events.insert(
{
auth: myGoogleAuth,
- calendarId: event.destinationCalendar?.externalId
- ? event.destinationCalendar.externalId
+ calendarId: calEventRaw.destinationCalendar?.externalId
+ ? calEventRaw.destinationCalendar.externalId
: "primary",
requestBody: payload,
conferenceDataVersion: 1,
@@ -121,6 +119,22 @@ export default class GoogleCalendarService implements Calendar {
console.error("There was an error contacting google calendar service: ", err);
return reject(err);
}
+
+ calendar.events.patch({
+ // Update the same event but this time we know the hangout link
+ calendarId: calEventRaw.destinationCalendar?.externalId
+ ? calEventRaw.destinationCalendar.externalId
+ : "primary",
+ auth: myGoogleAuth,
+ eventId: event.data.id || "",
+ requestBody: {
+ description: getRichDescription({
+ ...calEventRaw,
+ additionInformation: { hangoutLink: event.data.hangoutLink || "" },
+ }),
+ },
+ });
+
return resolve({
uid: "",
...event.data,
diff --git a/packages/app-store/hubspotothercalendar/api/callback.ts b/packages/app-store/hubspotothercalendar/api/callback.ts
index a99a7ee7d4..b3483e45b9 100644
--- a/packages/app-store/hubspotothercalendar/api/callback.ts
+++ b/packages/app-store/hubspotothercalendar/api/callback.ts
@@ -3,6 +3,7 @@ import { TokenResponseIF } from "@hubspot/api-client/lib/codegen/oauth/models/To
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
+import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
@@ -52,5 +53,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const state = decodeOAuthState(req);
- res.redirect(state?.returnTo ?? "/apps/installed");
+ res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed");
}
diff --git a/packages/app-store/office365calendar/api/callback.ts b/packages/app-store/office365calendar/api/callback.ts
index 940f74f062..91e04b4c6e 100644
--- a/packages/app-store/office365calendar/api/callback.ts
+++ b/packages/app-store/office365calendar/api/callback.ts
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { BASE_URL } from "@calcom/lib/constants";
+import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
@@ -62,5 +63,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const state = decodeOAuthState(req);
- return res.redirect(state?.returnTo ?? "/apps/installed");
+ return res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed");
}
diff --git a/packages/app-store/office365video/api/callback.ts b/packages/app-store/office365video/api/callback.ts
index 2ef3f1a127..94e26cbd0d 100644
--- a/packages/app-store/office365video/api/callback.ts
+++ b/packages/app-store/office365video/api/callback.ts
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { BASE_URL } from "@calcom/lib/constants";
+import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
@@ -63,5 +64,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const state = decodeOAuthState(req);
- return res.redirect(state?.returnTo ?? "/apps/installed");
+ return res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed");
}
diff --git a/packages/app-store/slackmessaging/api/add.ts b/packages/app-store/slackmessaging/api/add.ts
index a276d93134..6796e0aff3 100644
--- a/packages/app-store/slackmessaging/api/add.ts
+++ b/packages/app-store/slackmessaging/api/add.ts
@@ -4,7 +4,7 @@ import { stringify } from "querystring";
import prisma from "@calcom/prisma";
const client_id = process.env.SLACK_CLIENT_ID;
-const scopes = ["commands", "users:read", "users:read.email", "chat:write.public"];
+const scopes = ["commands", "users:read", "users:read.email", "chat:write", "chat:write.public"];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
diff --git a/packages/core/builders/CalendarEvent/builder.ts b/packages/core/builders/CalendarEvent/builder.ts
index d6ff81084d..11c55cdce6 100644
--- a/packages/core/builders/CalendarEvent/builder.ts
+++ b/packages/core/builders/CalendarEvent/builder.ts
@@ -5,6 +5,7 @@ import { v5 as uuidv5 } from "uuid";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
+import { CalendarEvent } from "@calcom/types/Calendar";
import { CalendarEventClass } from "./class";
@@ -124,6 +125,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
slug: true,
},
},
+ description: true,
slug: true,
teamId: true,
title: true,
@@ -263,6 +265,10 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
this.calendarEvent.description = description;
}
+ public setNotes(notes: CalendarEvent["additionalNotes"]) {
+ this.calendarEvent.additionalNotes = notes;
+ }
+
public setCancellationReason(cancellationReason: CalendarEventClass["cancellationReason"]) {
this.calendarEvent.cancellationReason = cancellationReason;
}
diff --git a/packages/core/builders/CalendarEvent/class.ts b/packages/core/builders/CalendarEvent/class.ts
index d433bda236..29e8b963dc 100644
--- a/packages/core/builders/CalendarEvent/class.ts
+++ b/packages/core/builders/CalendarEvent/class.ts
@@ -21,6 +21,7 @@ class CalendarEventClass implements CalendarEvent {
cancellationReason?: string | null;
rejectionReason?: string | null;
hideCalendarNotes?: boolean;
+ additionalNotes?: string | null | undefined;
constructor(initProps?: CalendarEvent) {
// If more parameters are given we update this
diff --git a/packages/core/builders/CalendarEvent/director.ts b/packages/core/builders/CalendarEvent/director.ts
index 0eda82f90c..b33311b8bf 100644
--- a/packages/core/builders/CalendarEvent/director.ts
+++ b/packages/core/builders/CalendarEvent/director.ts
@@ -27,6 +27,8 @@ export class CalendarEventDirector {
this.builder.setLocation(this.existingBooking.location);
this.builder.setUId(this.existingBooking.uid);
this.builder.setCancellationReason(this.cancellationReason);
+ this.builder.setDescription(this.builder.eventType.description);
+ this.builder.setNotes(this.existingBooking.description);
this.builder.buildRescheduleLink(this.existingBooking.uid);
} else {
throw new Error("buildForRescheduleEmail.missing.params.required");
diff --git a/packages/lib/CalEventParser.ts b/packages/lib/CalEventParser.ts
index 888a3748ca..69b9d14ba3 100644
--- a/packages/lib/CalEventParser.ts
+++ b/packages/lib/CalEventParser.ts
@@ -46,6 +46,9 @@ ${organizer + attendees}
};
export const getAdditionalNotes = (calEvent: CalendarEvent) => {
+ if (!calEvent.additionalNotes) {
+ return "";
+ }
return `
${calEvent.organizer.language.translate("additional_notes")}:
${calEvent.additionalNotes}
@@ -53,6 +56,9 @@ ${calEvent.additionalNotes}
};
export const getDescription = (calEvent: CalendarEvent) => {
+ if (!calEvent.description) {
+ return "";
+ }
return `\n${calEvent.attendees[0].language.translate("description")}
${calEvent.description}
`;
diff --git a/packages/lib/getSafeRedirectUrl.ts b/packages/lib/getSafeRedirectUrl.ts
new file mode 100644
index 0000000000..c7beb6cbd9
--- /dev/null
+++ b/packages/lib/getSafeRedirectUrl.ts
@@ -0,0 +1,16 @@
+import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
+
+// It ensures that redirection URL safe where it is accepted through a query params or other means where user can change it.
+export const getSafeRedirectUrl = (url: string | undefined) => {
+ url = url || "";
+ if (url.search(/^https?:\/\//) === -1) {
+ throw new Error("Pass an absolute URL");
+ }
+
+ // Avoid open redirection security vulnerability
+ if (!url.startsWith(WEBAPP_URL) && !url.startsWith(WEBSITE_URL)) {
+ url = `${WEBAPP_URL}/`;
+ }
+
+ return url;
+};
diff --git a/packages/prisma/migrations/20220420152505_add_hashed_event_url/migration.sql b/packages/prisma/migrations/20220420152505_add_hashed_event_url/migration.sql
new file mode 100644
index 0000000000..6890eb7b3d
--- /dev/null
+++ b/packages/prisma/migrations/20220420152505_add_hashed_event_url/migration.sql
@@ -0,0 +1,23 @@
+-- DropForeignKey
+ALTER TABLE "BookingReference" DROP CONSTRAINT "BookingReference_bookingId_fkey";
+
+-- CreateTable
+CREATE TABLE "HashedLink" (
+ "id" SERIAL NOT NULL,
+ "link" TEXT NOT NULL,
+ "eventTypeId" INTEGER NOT NULL,
+
+ CONSTRAINT "HashedLink_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "HashedLink_link_key" ON "HashedLink"("link");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "HashedLink_eventTypeId_key" ON "HashedLink"("eventTypeId");
+
+-- AddForeignKey
+ALTER TABLE "BookingReference" ADD CONSTRAINT "BookingReference_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "HashedLink" ADD CONSTRAINT "HashedLink_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
\ No newline at end of file
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index f468da8e3f..a3e68dc8fa 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -45,6 +45,7 @@ model EventType {
userId Int?
team Team? @relation(fields: [teamId], references: [id])
teamId Int?
+ hashedLink HashedLink?
bookings Booking[]
availability Availability[]
webhooks Webhook[]
@@ -418,6 +419,13 @@ model ApiKey {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
}
+model HashedLink {
+ id Int @id @default(autoincrement())
+ link String @unique()
+ eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
+ eventTypeId Int @unique
+}
+
model Account {
id String @id @default(cuid())
userId Int