From 34d3aac4b0b61583cf66d9c8c58981d799d67e08 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 27 Apr 2022 11:57:39 +0100 Subject: [PATCH 01/18] Fix scope (#2625) --- apps/website | 2 +- packages/app-store/slackmessaging/api/add.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/slackmessaging/api/add.ts b/packages/app-store/slackmessaging/api/add.ts index c8afab68b6..7fe78af6d8 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.public", "chat.write"]; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session?.user?.id) { From cf346f6aa3e4adeea465bf05634d827b7e42d934 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Wed, 27 Apr 2022 16:38:13 +0530 Subject: [PATCH 02/18] Reduce Payload for Event-Types[Avoid 500] (#2627) Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --- apps/web/next.config.js | 4 ++ apps/web/pages/api/user/avatar.ts | 53 +++++++++++++++----- apps/web/pages/event-types/index.tsx | 74 +++++++++++++++------------- apps/web/server/routers/viewer.tsx | 4 -- 4 files changed, 84 insertions(+), 51 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 02ebea1769..65de64f75d 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -80,6 +80,10 @@ const nextConfig = { source: "/:user/avatar.png", destination: "/api/user/avatar?username=:user", }, + { + source: "/team/:teamname/avatar.png", + destination: "/api/user/avatar?teamname=:teamname", + }, ]; }, async redirects() { diff --git a/apps/web/pages/api/user/avatar.ts b/apps/web/pages/api/user/avatar.ts index 7a85a1af50..8bd856d8f5 100644 --- a/apps/web/pages/api/user/avatar.ts +++ b/apps/web/pages/api/user/avatar.ts @@ -1,31 +1,60 @@ import crypto from "crypto"; import type { NextApiRequest, NextApiResponse } from "next"; +import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; import prisma from "@lib/prisma"; import { defaultAvatarSrc } from "@lib/profile"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { // const username = req.url?.substring(1, req.url.lastIndexOf("/")); const username = req.query.username as string; - const user = await prisma.user.findUnique({ - where: { - username: username, - }, - select: { - avatar: true, - email: true, - }, - }); + const teamname = req.query.teamname as string; + let identity; + if (username) { + const user = await prisma.user.findUnique({ + where: { + username: username, + }, + select: { + avatar: true, + email: true, + }, + }); + identity = { + name: username, + email: user?.email, + avatar: user?.avatar, + }; + } else if (teamname) { + const team = await prisma.team.findUnique({ + where: { + slug: teamname, + }, + select: { + logo: true, + }, + }); + identity = { + name: teamname, + shouldDefaultBeNameBased: true, + avatar: team?.logo, + }; + } const emailMd5 = crypto .createHash("md5") - .update((user?.email as string) || "guest@example.com") + .update((identity?.email as string) || "guest@example.com") .digest("hex"); - const img = user?.avatar; + const img = identity?.avatar; if (!img) { + let defaultSrc = defaultAvatarSrc({ md5: emailMd5 }); + if (identity?.shouldDefaultBeNameBased) { + defaultSrc = getPlaceholderAvatar(null, identity.name); + } res.writeHead(302, { - Location: defaultAvatarSrc({ md5: emailMd5 }), + Location: defaultSrc, }); + res.end(); } else if (!img.includes("data:image")) { res.writeHead(302, { diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 75c366f570..ba010aee22 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -18,6 +18,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import React, { Fragment, useEffect, useState } from "react"; +import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; import { Button } from "@calcom/ui"; @@ -452,45 +453,48 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL ); }; -const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => ( -
- - - - - -
+const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => { + console.log(profile.slug); + return ( +
- {profile?.name || ""} + + + - {membershipCount && ( - - - - - - {membershipCount} - - - - - )} - {profile?.slug && ( - - {`${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace( - "https://", - "" - )}/${profile.slug}`} +
+ + {profile?.name || ""} - )} + {membershipCount && ( + + + + + + {membershipCount} + + + + + )} + {profile?.slug && ( + + {`${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace( + "https://", + "" + )}/${profile.slug}`} + + )} +
-
-); + ); +}; const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypeProps) => { const { t } = useLocale(); diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index 7719da5d90..084711c227 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -137,7 +137,6 @@ const loggedInViewerRouter = createProtectedRouter() select: { id: true, username: true, - avatar: true, name: true, }, }, @@ -154,7 +153,6 @@ const loggedInViewerRouter = createProtectedRouter() startTime: true, endTime: true, bufferTime: true, - avatar: true, plan: true, teams: { where: { @@ -230,7 +228,6 @@ const loggedInViewerRouter = createProtectedRouter() profile: { slug: typeof user["username"]; name: typeof user["name"]; - image: typeof user["avatar"]; }; metadata: { membershipCount: number; @@ -255,7 +252,6 @@ const loggedInViewerRouter = createProtectedRouter() profile: { slug: user.username, name: user.name, - image: user.avatar, }, eventTypes: _.orderBy(mergedEventTypes, ["position", "id"], ["desc", "asc"]), metadata: { From 66f3fd2e07683699d320e55aa32fe01728b00173 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Wed, 27 Apr 2022 14:15:44 +0200 Subject: [PATCH 03/18] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a2710e6ac..607e85154c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,8 @@ Fixes # (issue) +Loom Video: https://www.loom.com/ + ## Type of change From ae1f35f51540169af749b00748cc3454fc8e8243 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Wed, 27 Apr 2022 14:16:31 +0200 Subject: [PATCH 04/18] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 607e85154c..9e58c5da24 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,7 @@ Fixes # (issue) + Loom Video: https://www.loom.com/ ## Type of change From 82d77dc10f5b6251e25c94ba72198ac053594302 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Wed, 27 Apr 2022 19:58:36 +0530 Subject: [PATCH 05/18] Make sure that absolute URL is of WEBAPP only (#2624) --- apps/web/pages/auth/login.tsx | 6 +++++- .../app-store/googlecalendar/api/callback.ts | 5 ++--- .../hubspotothercalendar/api/callback.ts | 3 ++- .../app-store/office365calendar/api/callback.ts | 3 ++- .../app-store/office365video/api/callback.ts | 3 ++- packages/lib/getSafeRedirectUrl.ts | 16 ++++++++++++++++ 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 packages/lib/getSafeRedirectUrl.ts diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 51ee2e606b..94f25ea0fd 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { EmailField, Form, PasswordField } from "@calcom/ui/form/fields"; @@ -61,12 +62,15 @@ export default function Login({ let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : ""; - // If not absolute URL, make it absolute if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1); + + // If not absolute URL, make it absolute if (!/^https?:\/\//.test(callbackUrl)) { callbackUrl = `${WEBAPP_URL}/${callbackUrl}`; } + callbackUrl = getSafeRedirectUrl(callbackUrl); + const LoginFooter = ( {t("dont_have_an_account")}{" "} 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/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/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; +}; From 7d98c0bb1c5d791b884e3d5e7389d26ab18c91d3 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Wed, 27 Apr 2022 20:28:04 +0530 Subject: [PATCH 06/18] Add Event Types Test (#2610) * Add Event Types Test * Accept license for tests * Accept license on preview * Remove debugging code * Add License consent flag * Test fixes * Update playwright.config.ts * Update webhookResponse-chromium.txt Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars --- apps/web/ee/components/LicenseBanner.tsx | 2 +- apps/web/pages/event-types/[type].tsx | 9 +++-- apps/web/playwright/event-types.test.ts | 39 +++++++++++++++++++ .../webhookResponse-chromium.txt | 2 +- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/web/ee/components/LicenseBanner.tsx b/apps/web/ee/components/LicenseBanner.tsx index 16f2232c74..8e547a734b 100644 --- a/apps/web/ee/components/LicenseBanner.tsx +++ b/apps/web/ee/components/LicenseBanner.tsx @@ -20,7 +20,7 @@ export default function LicenseBanner() { - Acquire a commercial license to remove these terms by visiting: cal.com/sales NEXT_PUBLIC_LICENSE_CONSENT='' */ - if (process.env.NEXT_PUBLIC_LICENSE_CONSENT === "agree") { + if (process.env.NEXT_PUBLIC_LICENSE_CONSENT === "agree" || process.env.NEXT_PUBLIC_IS_E2E) { return null; } diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index abef3a6113..a0f9609bf6 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -1117,7 +1117,10 @@ const EventTypePage = (props: inferSSRProps) => { open={advancedSettingsVisible} onOpenChange={() => setAdvancedSettingsVisible(!advancedSettingsVisible)}> <> - + ) => { {t("show_advanced_settings")} - + {/** * Only display calendar selector if user has connected calendars AND if it's not * a team event. Since we don't have logic to handle each attende calendar (for now). @@ -1663,7 +1666,7 @@ const EventTypePage = (props: inferSSRProps) => { -
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/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 From 6f0fcc9d1b36907b7e43d8238954954289847356 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 27 Apr 2022 16:19:04 +0100 Subject: [PATCH 07/18] Adding validation for name and email (#2612) --- apps/web/components/booking/pages/BookingPage.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 78805642de..ab686b288a 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -5,6 +5,7 @@ import { ExclamationIcon, InformationCircleIcon, } from "@heroicons/react/solid"; +import { zodResolver } from "@hookform/resolvers/zod"; import { EventTypeCustomInputType } from "@prisma/client"; import { useContracts } from "contexts/contractsContext"; import dayjs from "dayjs"; @@ -17,6 +18,7 @@ import { Controller, useForm, useWatch } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; +import { z } from "zod"; import { useIsEmbed, @@ -195,8 +197,14 @@ const BookingPage = ({ }; }; + const bookingFormSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), + }); + const bookingForm = useForm({ defaultValues: defaultValues(), + resolver: zodResolver(bookingFormSchema), // Since this isnt set to strict we only validate the fields in the schema }); const selectedLocation = useWatch({ @@ -389,7 +397,7 @@ const BookingPage = ({
Date: Wed, 27 Apr 2022 15:21:18 -0600 Subject: [PATCH 08/18] Fix book event form schema validation (#2633) --- apps/web/components/booking/pages/BookingPage.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index ab686b288a..0e23fa5481 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -197,14 +197,16 @@ const BookingPage = ({ }; }; - const bookingFormSchema = z.object({ - name: z.string().min(1), - email: z.string().email(), - }); + const bookingFormSchema = z + .object({ + name: z.string().min(1), + email: z.string().email(), + }) + .passthrough(); const bookingForm = useForm({ defaultValues: defaultValues(), - resolver: zodResolver(bookingFormSchema), // Since this isnt set to strict we only validate the fields in the schema + resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema }); const selectedLocation = useWatch({ From eea40c69f7a5d6cae5723bb8023f5d69edaa2811 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:34:04 -0400 Subject: [PATCH 09/18] Add new response if request contains account id (#2629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/ee/pages/api/integrations/stripepayment/webhook.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts index 1b29a7e884..7bddc5b339 100644 --- a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts +++ b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts @@ -174,6 +174,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET); + if (event.account) { + throw new HttpCode({ statusCode: 202, message: "Incoming connected account" }); + } + const handler = webhookHandlers[event.type]; if (handler) { await handler(event); From a6183e0ccf7ae72675abc39318e789678600e7e8 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:10:40 +0100 Subject: [PATCH 10/18] Unlock edit on reschedule (#2628) --- apps/web/components/booking/pages/BookingPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 0e23fa5481..ed59e2c76b 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -446,7 +446,6 @@ const BookingPage = ({ {...bookingForm.register("locationType", { required: true })} value={location.type} defaultChecked={selectedLocation === location.type} - disabled={disableInput} /> {locationLabels[location.type]} From af0d1980c6063c0185927bdcd7c9bc278cb675a2 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Thu, 28 Apr 2022 13:22:40 +0200 Subject: [PATCH 11/18] fixed layout in insalled apps (#2639) --- apps/web/components/AppsShell.tsx | 2 +- apps/web/pages/apps/installed.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/components/AppsShell.tsx b/apps/web/components/AppsShell.tsx index 810bc843c1..51f55c7448 100644 --- a/apps/web/components/AppsShell.tsx +++ b/apps/web/components/AppsShell.tsx @@ -24,7 +24,7 @@ export default function AppsShell({ children }: { children: React.ReactNode }) {
{status === "authenticated" && }
-
{children}
+
{children}
); } diff --git a/apps/web/pages/apps/installed.tsx b/apps/web/pages/apps/installed.tsx index fd2cf5648d..c39a0a6edb 100644 --- a/apps/web/pages/apps/installed.tsx +++ b/apps/web/pages/apps/installed.tsx @@ -256,7 +256,7 @@ function Web3Container() { return ( <> - +
From d1082e55a42640a2ee24386e951a54bf98c38697 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Thu, 28 Apr 2022 13:31:55 +0200 Subject: [PATCH 12/18] consistency for tablet booking page (#2640) --- apps/web/components/booking/pages/AvailabilityPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index d3edca60ad..296eb0da17 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -172,7 +172,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage )}> {/* mobile: details */}
-
+
-
+

{profile.name}

-
+

{eventType.title}

@@ -203,7 +203,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage

)}

- + {eventType.length} {t("minutes")}

{eventType.price > 0 && ( From f274c0bde39a9c3c7de8c11398d11d0bb45463b6 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Thu, 28 Apr 2022 13:42:37 +0100 Subject: [PATCH 13/18] Fixing Scope Name [Slack] (#2641) --- packages/app-store/slackmessaging/api/add.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store/slackmessaging/api/add.ts b/packages/app-store/slackmessaging/api/add.ts index 7fe78af6d8..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", "chat.write"]; +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) { From 99666440cfc6b0bd05bcab73bddba531155c5e98 Mon Sep 17 00:00:00 2001 From: Afzal Sayed <14029371+afzalsayed96@users.noreply.github.com> Date: Thu, 28 Apr 2022 19:26:10 +0530 Subject: [PATCH 14/18] Pass userId while creating event-type (#2599) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/server/routers/viewer/eventTypes.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/server/routers/viewer/eventTypes.tsx b/apps/web/server/routers/viewer/eventTypes.tsx index 48c3d04de4..7fc2d80d12 100644 --- a/apps/web/server/routers/viewer/eventTypes.tsx +++ b/apps/web/server/routers/viewer/eventTypes.tsx @@ -117,6 +117,7 @@ export const eventTypesRouter = createProtectedRouter() const data: Prisma.EventTypeCreateInput = { ...rest, + userId: teamId ? undefined : userId, users: { connect: { id: userId, From d960e03acf492bf637e818143004d9cd83b45792 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Thu, 28 Apr 2022 15:54:31 +0100 Subject: [PATCH 15/18] Fixes google meet url not appearing calendar invite (#2636) * Update cal description * Tidy up console logs Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../googlecalendar/lib/CalendarService.ts | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) 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, From 8e956893caa51370fb91c961a560a1141ce3b8bf Mon Sep 17 00:00:00 2001 From: alannnc Date: Thu, 28 Apr 2022 09:05:29 -0600 Subject: [PATCH 16/18] Fix emails and cal event descriptions (#2634) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/pages/api/book/request-reschedule.ts | 8 -------- apps/web/pages/cancel/[uid].tsx | 4 ++-- packages/core/builders/CalendarEvent/builder.ts | 6 ++++++ packages/core/builders/CalendarEvent/class.ts | 1 + packages/core/builders/CalendarEvent/director.ts | 2 ++ packages/lib/CalEventParser.ts | 6 ++++++ 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/web/pages/api/book/request-reschedule.ts b/apps/web/pages/api/book/request-reschedule.ts index 0c321678db..a4c96646bb 100644 --- a/apps/web/pages/api/book/request-reschedule.ts +++ b/apps/web/pages/api/book/request-reschedule.ts @@ -174,14 +174,6 @@ const handler = async ( } }); - // Creating cancelled event as placeholders in calendars, remove when virtual calendar handles it - const eventManager = new EventManager({ - credentials: userOwner.credentials, - destinationCalendar: userOwner.destinationCalendar, - }); - builder.calendarEvent.title = `Cancelled: ${builder.calendarEvent.title}`; - await eventManager.updateAndSetCancelledPlaceholder(builder.calendarEvent, bookingToReschedule); - // Send emails await sendRequestRescheduleEmail(builder.calendarEvent, { rescheduleLink: builder.rescheduleLink, diff --git a/apps/web/pages/cancel/[uid].tsx b/apps/web/pages/cancel/[uid].tsx index 538d81f69c..3f64966e18 100644 --- a/apps/web/pages/cancel/[uid].tsx +++ b/apps/web/pages/cancel/[uid].tsx @@ -101,8 +101,8 @@ export default function Type(props: inferSSRProps) { className="mb-5 sm:mb-6" />
- + +
+ + The URL will regenerate after each use + +
+
+ )} + + )} + /> +
beforeEventBuffer: true, afterEventBuffer: true, slotInterval: true, + hashedLink: true, successRedirectUrl: true, team: { select: { 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/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/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c679e79bbf..e900330a77 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!", diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index 084711c227..2894d3ce27 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -133,6 +133,7 @@ const loggedInViewerRouter = createProtectedRouter() currency: true, position: true, successRedirectUrl: true, + hashedLink: true, users: { select: { id: true, diff --git a/apps/web/server/routers/viewer/eventTypes.tsx b/apps/web/server/routers/viewer/eventTypes.tsx index 7fc2d80d12..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( @@ -214,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; @@ -250,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/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 41dc5d1a53..2c7e14e908 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[] @@ -406,6 +407,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 From 02fb15228b8bb142baa4cd27aaea47ce81f5eee4 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:45:27 -0400 Subject: [PATCH 18/18] Hotfix - change calendar error message (#2643) * Change calendar error message * Change calendar error message Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/components/integrations/CalendarListContainer.tsx | 2 +- apps/web/public/static/locales/en/common.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/components/integrations/CalendarListContainer.tsx b/apps/web/components/integrations/CalendarListContainer.tsx index dbb1f0366b..9204762023 100644 --- a/apps/web/components/integrations/CalendarListContainer.tsx +++ b/apps/web/components/integrations/CalendarListContainer.tsx @@ -139,7 +139,7 @@ function ConnectedCalendarsList(props: Props) { ) : ( Click Here to stop." + "impersonating_stop_instructions": "<0>Click Here to stop.", + "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions" }