From ec6b8971910c86cc96e642f1e5c58243c7e12891 Mon Sep 17 00:00:00 2001 From: Alex Johansson Date: Wed, 13 Oct 2021 13:35:25 +0200 Subject: [PATCH] integration page follow ups (#912) ### Internals - Replace `lodash.*` packages with plain `lodash` & replace `lodash.*` imports with `lodash/` - should have no impact on bundle size and opens up for us to use all of lodash - Update `viewer.me` to cherry-pick what we actually need on that query to avoid leaking extra context info - Update `getIntegrations` to never include `.key`-property to avoid leaking ### Visual - Update calendars so `primary` is displayed last - Update connected calendars so they are in ascending order in which you connected them --- .env.example | 1 - components/seo/head-seo.tsx | 2 +- environment.d.ts | 1 - lib/events/EventManager.ts | 2 +- .../Apple/components/AddAppleIntegration.tsx | 58 ++------------- lib/integrations/getIntegrations.ts | 45 ++++++++---- package.json | 11 ++- pages/api/integrations/googlecalendar/add.ts | 4 +- .../integrations/googlecalendar/callback.ts | 4 +- pages/auth/forgot-password/[id].tsx | 2 +- pages/auth/forgot-password/index.tsx | 2 +- pages/event-types/[type].tsx | 4 +- pages/getting-started.tsx | 6 +- pages/integrations/index.tsx | 33 +++++---- public/integrations/daily.svg | 17 +++++ server/createContext.ts | 3 + server/routers/viewer.tsx | 70 ++++++++++++++----- yarn.lock | 28 ++------ 18 files changed, 146 insertions(+), 147 deletions(-) create mode 100644 public/integrations/daily.svg diff --git a/.env.example b/.env.example index 028249407e..418388c30c 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,6 @@ NEXT_PUBLIC_LICENSE_CONSENT='' DATABASE_URL="postgresql://postgres:@localhost:5432/calendso?schema=public" GOOGLE_API_CREDENTIALS='secret' -GOOGLE_REDIRECT_URL='https://localhost:3000/integrations/googlecalendar/callback' BASE_URL='http://localhost:3000' NEXT_PUBLIC_APP_URL='http://localhost:3000' diff --git a/components/seo/head-seo.tsx b/components/seo/head-seo.tsx index f0446b7b80..41354e4760 100644 --- a/components/seo/head-seo.tsx +++ b/components/seo/head-seo.tsx @@ -1,4 +1,4 @@ -import merge from "lodash.merge"; +import merge from "lodash/merge"; import { NextSeo, NextSeoProps } from "next-seo"; import React from "react"; diff --git a/environment.d.ts b/environment.d.ts index 65a7dff1c6..c5a965c90d 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -3,7 +3,6 @@ declare namespace NodeJS { readonly CALENDSO_ENCRYPTION_KEY: string | undefined; readonly DATABASE_URL: string | undefined; readonly GOOGLE_API_CREDENTIALS: string | undefined; - readonly GOOGLE_REDIRECT_URL: string | undefined; readonly BASE_URL: string | undefined; readonly NEXT_PUBLIC_BASE_URL: string | undefined; readonly NEXT_PUBLIC_APP_URL: string | undefined; diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 1c444fe67b..ed76dc1411 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -1,6 +1,6 @@ import { Credential } from "@prisma/client"; import async from "async"; -import merge from "lodash.merge"; +import merge from "lodash/merge"; import { v5 as uuidv5 } from "uuid"; import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient"; diff --git a/lib/integrations/Apple/components/AddAppleIntegration.tsx b/lib/integrations/Apple/components/AddAppleIntegration.tsx index a18e62f167..ab901d223b 100644 --- a/lib/integrations/Apple/components/AddAppleIntegration.tsx +++ b/lib/integrations/Apple/components/AddAppleIntegration.tsx @@ -2,21 +2,17 @@ import React, { useState } from "react"; import { useForm } from "react-hook-form"; import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, DialogHeader, DialogProps, - Dialog, - DialogContent, - DialogClose, - DialogFooter, } from "@components/Dialog"; import { Form, TextField } from "@components/form/fields"; import { Alert } from "@components/ui/Alert"; import Button from "@components/ui/Button"; -type Props = { - onSubmit: () => void; -}; - export const ADD_APPLE_INTEGRATION_FORM_TITLE = "addAppleIntegration"; export function AddAppleIntegrationModal(props: DialogProps) { @@ -104,49 +100,3 @@ export function AddAppleIntegrationModal(props: DialogProps) { ); } - -/** - * @deprecated - */ -const AddAppleIntegration = React.forwardRef((props, ref) => { - const onSubmit = (event) => { - event.preventDefault(); - event.stopPropagation(); - - props.onSubmit(); - }; - - return ( -
-
- - -
-
- - -
-
- ); -}); - -AddAppleIntegration.displayName = "AddAppleIntegrationForm"; -export default AddAppleIntegration; diff --git a/lib/integrations/getIntegrations.ts b/lib/integrations/getIntegrations.ts index 831c47c194..0f5c6290e6 100644 --- a/lib/integrations/getIntegrations.ts +++ b/lib/integrations/getIntegrations.ts @@ -1,9 +1,10 @@ import { Prisma } from "@prisma/client"; +import _ from "lodash"; import { validJson } from "@lib/jsonUtils"; const credentialData = Prisma.validator()({ - select: { id: true, type: true, key: true }, + select: { id: true, type: true }, }); type CredentialData = Prisma.CredentialGetPayload; @@ -33,6 +34,14 @@ export const ALL_INTEGRATIONS = [ description: "Video Conferencing", variant: "conferencing", }, + { + installed: !!process.env.DAILY_API_KEY, + type: "daily_video", + title: "Daily.co Video", + imageSrc: "integrations/daily.svg", + description: "Video Conferencing", + variant: "conferencing", + }, { installed: true, type: "caldav_calendar", @@ -62,23 +71,33 @@ export const ALL_INTEGRATIONS = [ variant: "payment", }, ] as const; -function getIntegrations(credentials: CredentialData[]) { - const integrations = ALL_INTEGRATIONS.map((integration) => ({ - ...integration, - /** - * @deprecated use `credentials. - */ - credential: credentials.find((credential) => credential.type === integration.type) || null, - credentials: credentials.filter((credential) => credential.type === integration.type) || null, - })); + +function getIntegrations(userCredentials: CredentialData[]) { + const integrations = ALL_INTEGRATIONS.map((integration) => { + const credentials = userCredentials + .filter((credential) => credential.type === integration.type) + .map((credential) => _.pick(credential, ["id", "type"])); // ensure we don't leak `key` to frontend + + const credential: typeof credentials[number] | null = credentials[0] || null; + return { + ...integration, + /** + * @deprecated use `credentials` + */ + credential, + credentials, + }; + }); return integrations; } -export type IntegraionMeta = ReturnType; +export type IntegrationMeta = ReturnType; -export function hasIntegration(integrations: ReturnType, type: string): boolean { - return !!integrations.find((i) => i.type === type && !!i.installed && !!i.credential); +export function hasIntegration(integrations: IntegrationMeta, type: string): boolean { + return !!integrations.find( + (i) => i.type === type && !!i.installed && (type === "daily_video" || i.credentials.length > 0) + ); } export default getIntegrations; diff --git a/package.json b/package.json index 48c3feec2a..d0038c13f8 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "yarn": ">=1.19.0 < 2.0.0" }, "dependencies": { - "@headlessui/react": "^1.4.1", "@daily-co/daily-js": "^0.16.0", + "@headlessui/react": "^1.4.1", "@heroicons/react": "^1.0.4", "@hookform/resolvers": "^2.8.1", "@jitsu/sdk-js": "^2.2.4", @@ -61,9 +61,7 @@ "ical.js": "^1.4.0", "ics": "^2.31.0", "jimp": "^0.16.1", - "lodash.debounce": "^4.0.8", - "lodash.merge": "^4.6.2", - "lodash.throttle": "^4.1.1", + "lodash": "^4.17.21", "micro": "^9.3.4", "next": "^11.1.1", "next-auth": "^3.28.0", @@ -82,10 +80,10 @@ "react-multi-email": "^0.5.3", "react-phone-number-input": "^3.1.25", "react-query": "^3.23.1", + "react-router-dom": "^5.2.0", "react-select": "^4.3.1", "react-timezone-select": "^1.0.7", "react-use-intercom": "1.4.0", - "react-router-dom": "^5.2.0", "short-uuid": "^4.2.0", "stripe": "^8.168.0", "superjson": "1.7.5", @@ -100,8 +98,7 @@ "@types/async": "^3.2.7", "@types/bcryptjs": "^2.4.2", "@types/jest": "^27.0.1", - "@types/lodash.debounce": "^4.0.6", - "@types/lodash.merge": "^4.6.6", + "@types/lodash": "^4.14.175", "@types/micro": "^7.3.6", "@types/node": "^16.6.1", "@types/nodemailer": "^6.4.4", diff --git a/pages/api/integrations/googlecalendar/add.ts b/pages/api/integrations/googlecalendar/add.ts index cb624f2bf4..f7a5768887 100644 --- a/pages/api/integrations/googlecalendar/add.ts +++ b/pages/api/integrations/googlecalendar/add.ts @@ -20,8 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } // Get token from Google Calendar API - const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web; - const redirect_uri = process.env.GOOGLE_REDIRECT_URL || redirect_uris[0]; + const { client_secret, client_id } = JSON.parse(credentials).web; + const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback"; const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const authUrl = oAuth2Client.generateAuthUrl({ diff --git a/pages/api/integrations/googlecalendar/callback.ts b/pages/api/integrations/googlecalendar/callback.ts index 62066d6417..6b6974cb36 100644 --- a/pages/api/integrations/googlecalendar/callback.ts +++ b/pages/api/integrations/googlecalendar/callback.ts @@ -25,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return; } - const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web; - const redirect_uri = process.env.GOOGLE_REDIRECT_URL || redirect_uris[0]; + const { client_secret, client_id } = JSON.parse(credentials).web; + const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback"; const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const token = await oAuth2Client.getToken(code); const key = token.res?.data; diff --git a/pages/auth/forgot-password/[id].tsx b/pages/auth/forgot-password/[id].tsx index b031de8e74..27d757618c 100644 --- a/pages/auth/forgot-password/[id].tsx +++ b/pages/auth/forgot-password/[id].tsx @@ -1,6 +1,6 @@ import { ResetPasswordRequest } from "@prisma/client"; import dayjs from "dayjs"; -import debounce from "lodash.debounce"; +import debounce from "lodash/debounce"; import { GetServerSidePropsContext } from "next"; import { getCsrfToken } from "next-auth/client"; import Link from "next/link"; diff --git a/pages/auth/forgot-password/index.tsx b/pages/auth/forgot-password/index.tsx index 5aa7b53be7..6d17b9fccc 100644 --- a/pages/auth/forgot-password/index.tsx +++ b/pages/auth/forgot-password/index.tsx @@ -1,4 +1,4 @@ -import debounce from "lodash.debounce"; +import debounce from "lodash/debounce"; import { getCsrfToken } from "next-auth/client"; import Link from "next/link"; import React from "react"; diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index b073553c6c..56e0c7a836 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -1247,7 +1247,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const locationOptions: OptionTypeBase[] = [ { value: LocationType.InPerson, label: "Link or In-person meeting" }, { value: LocationType.Phone, label: "Phone call" }, - { value: LocationType.Zoom, label: "Zoom Video", disabled: true }, ]; if (hasIntegration(integrations, "zoom_video")) { @@ -1257,8 +1256,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if (hasIntegration(integrations, "google_calendar")) { locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" }); } - const hasDailyIntegration = process.env.DAILY_API_KEY; - if (hasDailyIntegration) { + if (hasIntegration(integrations, "daily_video")) { locationOptions.push({ value: LocationType.Daily, label: "Daily.co Video" }); } const currency = diff --git a/pages/getting-started.tsx b/pages/getting-started.tsx index 83f34c21f1..9d3394e4a7 100644 --- a/pages/getting-started.tsx +++ b/pages/getting-started.tsx @@ -11,12 +11,12 @@ import classnames from "classnames"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; -import debounce from "lodash.debounce"; +import debounce from "lodash/debounce"; +import omit from "lodash/omit"; import { NextPageContext } from "next"; import { useSession } from "next-auth/client"; import Head from "next/head"; import { useRouter } from "next/router"; -import { Integration } from "pages/integrations/_new"; import React, { useEffect, useRef, useState } from "react"; import TimezoneSelect from "react-timezone-select"; @@ -691,7 +691,7 @@ export async function getServerSideProps(context: NextPageContext) { }, }); - integrations = getIntegrations(credentials); + integrations = getIntegrations(credentials).map((item) => omit(item, "key")); eventTypes = await prisma.eventType.findMany({ where: { diff --git a/pages/integrations/index.tsx b/pages/integrations/index.tsx index 913f351b29..0914debfca 100644 --- a/pages/integrations/index.tsx +++ b/pages/integrations/index.tsx @@ -1,4 +1,3 @@ -import { Maybe } from "@trpc/server"; import Image from "next/image"; import { ReactNode, useEffect, useState } from "react"; import { useMutation } from "react-query"; @@ -8,7 +7,7 @@ import classNames from "@lib/classNames"; import { AddAppleIntegrationModal } from "@lib/integrations/Apple/components/AddAppleIntegration"; import { AddCalDavIntegrationModal } from "@lib/integrations/CalDav/components/AddCalDavIntegration"; import showToast from "@lib/notification"; -import { inferQueryOutput, trpc } from "@lib/trpc"; +import { trpc } from "@lib/trpc"; import { Dialog } from "@components/Dialog"; import { List, ListItem, ListItemText, ListItemTitle } from "@components/List"; @@ -19,8 +18,6 @@ import Badge from "@components/ui/Badge"; import Button, { ButtonBaseProps } from "@components/ui/Button"; import Switch from "@components/ui/Switch"; -type IntegrationCalendar = inferQueryOutput<"viewer.integrations">["calendar"]["items"][number]; - function pluralize(opts: { num: number; plural: string; singular: string }) { if (opts.num === 0) { return opts.singular; @@ -47,10 +44,7 @@ function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnection ); } -function ConnectIntegration(props: { - type: IntegrationCalendar["type"]; - render: (renderProps: ButtonBaseProps) => JSX.Element; -}) { +function ConnectIntegration(props: { type: string; render: (renderProps: ButtonBaseProps) => JSX.Element }) { const { type } = props; const [isLoading, setIsLoading] = useState(false); const mutation = useMutation(async () => { @@ -154,14 +148,15 @@ function DisconnectIntegration(props: { function ConnectOrDisconnectIntegrationButton(props: { // - credential: Maybe<{ id: number }>; - type: IntegrationCalendar["type"]; + credentialIds: number[]; + type: string; installed: boolean; }) { - if (props.credential) { + const [credentialId] = props.credentialIds; + if (credentialId) { return ( ( } /> ); @@ -193,7 +196,7 @@ function IntegrationListItem(props: {
{props.title} -
+
{props.title} {props.description}
@@ -205,7 +208,7 @@ function IntegrationListItem(props: { } export function CalendarSwitch(props: { - type: IntegrationCalendar["type"]; + type: string; externalId: string; title: string; defaultSelected: boolean; @@ -353,7 +356,7 @@ export default function IntegrationsPage() { )} /> }> -
    +
      {item.calendars.map((cal) => ( + + + + + + diff --git a/server/createContext.ts b/server/createContext.ts index 004119ca9e..5eee4f6585 100644 --- a/server/createContext.ts +++ b/server/createContext.ts @@ -38,6 +38,9 @@ async function getUserFromSession({ session, req }: { session: Maybe; r type: true, key: true, }, + orderBy: { + id: "asc", + }, }, selectedCalendars: { select: { diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx index 515dc74e09..94c60c8b82 100644 --- a/server/routers/viewer.tsx +++ b/server/routers/viewer.tsx @@ -1,12 +1,13 @@ import { BookingStatus, Prisma } from "@prisma/client"; import { TRPCError } from "@trpc/server"; +import _ from "lodash"; import { getErrorFromUnknown } from "pages/_error"; import { z } from "zod"; import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername"; import { checkRegularUsername } from "@lib/core/checkRegularUsername"; -import getIntegrations, { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations"; +import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations"; import slugify from "@lib/slugify"; import { getCalendarAdapterOrNull } from "../../lib/calendarClient"; @@ -20,7 +21,34 @@ const checkUsername = export const viewerRouter = createProtectedRouter() .query("me", { resolve({ ctx }) { - return ctx.user; + const { + // pick only the part we want to expose in the API + id, + name, + username, + email, + startTime, + endTime, + bufferTime, + locale, + avatar, + createdDate, + completedOnboarding, + } = ctx.user; + const me = { + id, + name, + username, + email, + startTime, + endTime, + bufferTime, + locale, + avatar, + createdDate, + completedOnboarding, + }; + return me; }, }) .query("bookings", { @@ -98,11 +126,17 @@ export const viewerRouter = createProtectedRouter() async resolve({ ctx }) { const { user } = ctx; const { credentials } = user; - const integrations = getIntegrations(credentials); - function countActive(items: { credentials: unknown[] }[]) { - return items.reduce((acc, item) => acc + item.credentials.length, 0); + function countActive(items: { credentialIds: unknown[] }[]) { + return items.reduce((acc, item) => acc + item.credentialIds.length, 0); } + const integrations = ALL_INTEGRATIONS.map((integration) => ({ + ...integration, + credentialIds: credentials + .filter((credential) => credential.type === integration.type) + .map((credential) => credential.id), + })); + // `flatMap()` these work like `.filter()` but infers the types correctly const conferencing = integrations.flatMap((item) => (item.variant === "conferencing" ? [item] : [])); const payment = integrations.flatMap((item) => (item.variant === "payment" ? [item] : [])); const calendar = integrations.flatMap((item) => (item.variant === "calendar" ? [item] : [])); @@ -126,25 +160,24 @@ export const viewerRouter = createProtectedRouter() const connectedCalendars = await Promise.all( calendarCredentials.map(async (item) => { const { adapter, integration, credential } = item; + + const credentialId = credential.id; try { - const _calendars = await adapter.listCalendars(); - const calendars = _calendars.map((cal) => ({ - ...cal, - isSelected: !!user.selectedCalendars.find((selected) => selected.externalId === cal.externalId), - })); + const cals = await adapter.listCalendars(); + const calendars = _(cals) + .map((cal) => ({ + ...cal, + isSelected: user.selectedCalendars.some((selected) => selected.externalId === cal.externalId), + })) + .sortBy(["primary"]) + .value(); const primary = calendars.find((item) => item.primary) ?? calendars[0]; if (!primary) { - return { - integration, - credentialId: credential.id, - error: { - message: "No primary calendar found", - }, - }; + throw new Error("No primary calendar found"); } return { integration, - credentialId: credential.id, + credentialId, primary, calendars, }; @@ -152,6 +185,7 @@ export const viewerRouter = createProtectedRouter() const error = getErrorFromUnknown(_error); return { integration, + credentialId, error: { message: error.message, }, diff --git a/yarn.lock b/yarn.lock index b495ef81c6..a6a002f823 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1790,26 +1790,14 @@ version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" -"@types/lodash.debounce@^4.0.6": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60" - dependencies: - "@types/lodash" "*" - -"@types/lodash.merge@^4.6.6": - version "4.6.6" - resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.6.tgz#b84b403c1d31bc42d51772d1cd5557fa008cd3d6" - dependencies: - "@types/lodash" "*" - -"@types/lodash@*", "@types/lodash@^4.14.165": - version "4.14.175" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45" - "@types/lodash@4.14.168": version "4.14.168" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" +"@types/lodash@^4.14.165", "@types/lodash@^4.14.175": + version "4.14.175" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45" + "@types/micro@^7.3.6": version "7.3.6" resolved "https://registry.yarnpkg.com/@types/micro/-/micro-7.3.6.tgz#7d68eb5a780ac4761e3b80687b4ee7328ebc3f2e" @@ -5712,10 +5700,6 @@ lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -5757,10 +5741,6 @@ lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - lodash.topath@^4.5.2: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"