From ac9b2d05771850205f9dd440833d3aceb8934bac Mon Sep 17 00:00:00 2001 From: alannnc Date: Mon, 31 Oct 2022 15:06:03 -0700 Subject: [PATCH] Feature/invalid credentials (#5120) * Fixing types from handleErrorJson usage and Credential * Replace credential prisma type for a better suitable * Improvements on zoom video adapter * Renamed extendedCredentialType and put it in a best suited file * Frontend display invalid credential * Fix styles and text * Fix type required for fake daily credentials --- .../components/apps/IntegrationListItem.tsx | 13 +++ apps/web/pages/apps/installed/[category].tsx | 65 ++++++----- apps/web/public/static/locales/en/common.json | 15 +-- packages/app-store/_utils/getCalendar.ts | 5 +- .../applecalendar/lib/CalendarService.ts | 5 +- .../caldavcalendar/lib/CalendarService.ts | 5 +- .../lib/CalendarService.ts | 4 +- .../dailyvideo/lib/VideoApiAdapter.ts | 5 +- .../lib/CalendarService.ts | 4 +- .../lib/CalendarService.ts | 4 +- .../exchangecalendar/lib/CalendarService.ts | 4 +- .../googlecalendar/lib/CalendarService.ts | 7 +- .../lib/CalendarService.ts | 6 +- .../huddle01video/lib/VideoApiAdapter.ts | 18 +-- .../larkcalendar/lib/CalendarService.ts | 9 +- .../office365calendar/lib/CalendarService.ts | 35 ++++-- .../office365video/lib/VideoApiAdapter.ts | 28 +++-- .../tandemvideo/lib/VideoApiAdapter.ts | 76 ++++++++----- .../zoomvideo/lib/VideoApiAdapter.ts | 103 +++++++++++++----- packages/core/CalendarManager.ts | 25 +++-- packages/core/EventManager.ts | 15 ++- packages/core/videoClient.ts | 13 +-- packages/lib/CalendarService.ts | 5 +- packages/lib/errors.ts | 9 +- .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + packages/trpc/server/createContext.ts | 1 + packages/trpc/server/routers/viewer.tsx | 16 ++- packages/types/Credential.d.ts | 18 +++ packages/types/VideoApiAdapter.d.ts | 5 +- packages/ui/v2/core/Shell.tsx | 2 +- 31 files changed, 334 insertions(+), 189 deletions(-) create mode 100644 packages/prisma/migrations/20221017205314_add_invalid_field_credential_table/migration.sql create mode 100644 packages/types/Credential.d.ts diff --git a/apps/web/components/apps/IntegrationListItem.tsx b/apps/web/components/apps/IntegrationListItem.tsx index dc12aef017..03198de203 100644 --- a/apps/web/components/apps/IntegrationListItem.tsx +++ b/apps/web/components/apps/IntegrationListItem.tsx @@ -2,6 +2,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { ReactNode, useEffect, useState } from "react"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Icon } from "@calcom/ui"; import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List"; import classNames from "@lib/classNames"; @@ -17,7 +19,9 @@ function IntegrationListItem(props: { logo: string; destination?: boolean; separate?: boolean; + invalidCredential?: boolean; }): JSX.Element { + const { t } = useLocale(); const router = useRouter(); const { hl } = router.query; const [highlight, setHighlight] = useState(hl === props.slug); @@ -44,6 +48,15 @@ function IntegrationListItem(props: { {props.name || title} {props.description} + {/* Alert error that key stopped working. */} + {props.invalidCredential && ( +
+ + + {t("invalid_credential")} + +
+ )}
{props.actions}
diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index f6a9f63745..440a3bca7e 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -1,4 +1,3 @@ -import { InferGetServerSidePropsType } from "next"; import { useRouter } from "next/router"; import z from "zod"; @@ -10,7 +9,6 @@ import { inferQueryOutput, trpc } from "@calcom/trpc/react"; import { App } from "@calcom/types/App"; import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps"; import { Icon } from "@calcom/ui/Icon"; -import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader"; import { Alert } from "@calcom/ui/v2/core/Alert"; import Button from "@calcom/ui/v2/core/Button"; import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen"; @@ -23,16 +21,19 @@ import { QueryCell } from "@lib/QueryCell"; import { CalendarListContainer } from "@components/apps/CalendarListContainer"; import IntegrationListItem from "@components/apps/IntegrationListItem"; +import SkeletonLoader from "@components/v2/availability/SkeletonLoader"; function ConnectOrDisconnectIntegrationButton(props: { credentialIds: number[]; type: App["type"]; isGlobal?: boolean; installed?: boolean; + invalidCredentialIds?: number[]; }) { + const { type, credentialIds, isGlobal, installed, invalidCredentialIds } = props; const { t } = useLocale(); - const [credentialId] = props.credentialIds; - const type = props.type; + const [credentialId] = credentialIds; + const utils = trpc.useContext(); const handleOpenChange = () => { utils.invalidateQueries(["viewer.integrations"]); @@ -49,6 +50,7 @@ function ConnectOrDisconnectIntegrationButton(props: { /> ); } + return ( ); } - if (!props.installed) { + + if (!installed) { return (
@@ -66,7 +69,7 @@ function ConnectOrDisconnectIntegrationButton(props: { ); } /** We don't need to "Connect", just show that it's installed */ - if (props.isGlobal) { + if (isGlobal) { return (

{t("default")}

@@ -75,7 +78,7 @@ function ConnectOrDisconnectIntegrationButton(props: { } return ( (
- }> - - - ))} + {data.items + .filter((item) => item.invalidCredentialIds) + .map((item) => ( + 0} + actions={ +
+ +
+ }> + +
+ ))} ); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 439830059a..207298545b 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -140,7 +140,7 @@ "slide_zoom_drag_instructions": "Slide to zoom, drag to reposition", "view_notifications": "View notifications", "view_public_page": "View public page", - "copy_public_page_link":"Copy public page link", + "copy_public_page_link": "Copy public page link", "sign_out": "Sign out", "add_another": "Add another", "install_another": "Install another", @@ -1266,9 +1266,9 @@ "seats": "seats", "every_app_published": "Every app published on the Cal.com App Store is open source and thoroughly tested via peer reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are published by Cal.com. If you encounter inappropriate content or behaviour please report it.", "report_app": "Report app", - "limit_booking_frequency":"Limit booking frequency", - "limit_booking_frequency_description":"Limit how many times this event can be booked", - "add_limit":"Add Limit", + "limit_booking_frequency": "Limit booking frequency", + "limit_booking_frequency_description": "Limit how many times this event can be booked", + "add_limit": "Add Limit", "team_name_required": "Team name required", "show_attendees": "Share attendee information between guests", "how_additional_inputs_as_variables": "How to use Additional Inputs as Variables", @@ -1332,11 +1332,12 @@ "saml_sp_entity_id_copied": "SP Entity ID copied!", "saml_btn_configure": "Configure", "add_calendar": "Add Calendar", - "limit_future_bookings":"Limit future bookings", - "limit_future_bookings_description":"Limit how far in the future this event can be booked", + "limit_future_bookings": "Limit future bookings", + "limit_future_bookings_description": "Limit how far in the future this event can be booked", "no_event_types": "No event types setup", "no_event_types_description": "{{name}} has not setup any event types for you to book.", "number_sms_notifications": "Phone number (SMS\u00a0notifications)", "attendee_email_workflow": "Attendee email", - "attendee_email_info": "The person booking's email" + "attendee_email_info": "The person booking's email", + "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again." } diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 745085335c..742ac51a7a 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -1,13 +1,12 @@ -import { Credential } from "@prisma/client"; - import logger from "@calcom/lib/logger"; import type { Calendar } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import appStore from ".."; const log = logger.getChildLogger({ prefix: ["CalendarManager"] }); -export const getCalendar = (credential: Credential | null): Calendar | null => { +export const getCalendar = (credential: CredentialPayload | null): Calendar | null => { if (!credential || !credential.key) return null; const { type: calendarType } = credential; const calendarApp = appStore[calendarType.split("_").join("") as keyof typeof appStore]; diff --git a/packages/app-store/applecalendar/lib/CalendarService.ts b/packages/app-store/applecalendar/lib/CalendarService.ts index 278a447528..2b7aac1291 100644 --- a/packages/app-store/applecalendar/lib/CalendarService.ts +++ b/packages/app-store/applecalendar/lib/CalendarService.ts @@ -1,9 +1,8 @@ -import { Credential } from "@prisma/client"; - import CalendarService from "@calcom/lib/CalendarService"; +import { CredentialPayload } from "@calcom/types/Credential"; export default class AppleCalendarService extends CalendarService { - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { super(credential, "apple_calendar", "https://caldav.icloud.com"); } } diff --git a/packages/app-store/caldavcalendar/lib/CalendarService.ts b/packages/app-store/caldavcalendar/lib/CalendarService.ts index 42e1c23d8b..13bc20daf0 100644 --- a/packages/app-store/caldavcalendar/lib/CalendarService.ts +++ b/packages/app-store/caldavcalendar/lib/CalendarService.ts @@ -1,9 +1,8 @@ -import { Credential } from "@prisma/client"; - import CalendarService from "@calcom/lib/CalendarService"; +import { CredentialPayload } from "@calcom/types/Credential"; export default class CalDavCalendarService extends CalendarService { - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { super(credential, "caldav_calendar"); } } diff --git a/packages/app-store/closecomothercalendar/lib/CalendarService.ts b/packages/app-store/closecomothercalendar/lib/CalendarService.ts index dcd07ef1a5..276cb94026 100644 --- a/packages/app-store/closecomothercalendar/lib/CalendarService.ts +++ b/packages/app-store/closecomothercalendar/lib/CalendarService.ts @@ -1,4 +1,3 @@ -import { Credential } from "@prisma/client"; import z from "zod"; import CloseCom, { CloseComFieldOptions } from "@calcom/lib/CloseCom"; @@ -12,6 +11,7 @@ import type { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; const apiKeySchema = z.object({ encrypted: z.string(), @@ -55,7 +55,7 @@ export default class CloseComCalendarService implements Calendar { private closeCom: CloseCom; private log: typeof logger; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "closecom_other_calendar"; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts index 4c6f503a1d..10378e3ccf 100644 --- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts @@ -1,8 +1,8 @@ -import { Credential } from "@prisma/client"; import { z } from "zod"; import { handleErrorsJson } from "@calcom/lib/errors"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import type { PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -53,12 +53,13 @@ const meetingTokenSchema = z.object({ }); /** @deprecated use metadata on index file */ -export const FAKE_DAILY_CREDENTIAL: Credential = { +export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = { id: +new Date().getTime(), type: "daily_video", key: { apikey: process.env.DAILY_API_KEY }, userId: +new Date().getTime(), appId: "daily-video", + invalid: false, }; const fetcher = async (endpoint: string, init?: RequestInit | undefined) => { diff --git a/packages/app-store/exchange2013calendar/lib/CalendarService.ts b/packages/app-store/exchange2013calendar/lib/CalendarService.ts index 896c494de9..fcf029edf4 100644 --- a/packages/app-store/exchange2013calendar/lib/CalendarService.ts +++ b/packages/app-store/exchange2013calendar/lib/CalendarService.ts @@ -1,4 +1,3 @@ -import { Credential } from "@prisma/client"; import { Appointment, Attendee, @@ -32,6 +31,7 @@ import { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; export default class ExchangeCalendarService implements Calendar { private url = ""; @@ -40,7 +40,7 @@ export default class ExchangeCalendarService implements Calendar { private readonly exchangeVersion: ExchangeVersion; private credentials: Record; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "exchange2013_calendar"; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); diff --git a/packages/app-store/exchange2016calendar/lib/CalendarService.ts b/packages/app-store/exchange2016calendar/lib/CalendarService.ts index c70d2fc8b7..71868477c3 100644 --- a/packages/app-store/exchange2016calendar/lib/CalendarService.ts +++ b/packages/app-store/exchange2016calendar/lib/CalendarService.ts @@ -1,4 +1,3 @@ -import { Credential } from "@prisma/client"; import { Appointment, Attendee, @@ -32,6 +31,7 @@ import { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; export default class ExchangeCalendarService implements Calendar { private url = ""; @@ -40,7 +40,7 @@ export default class ExchangeCalendarService implements Calendar { private readonly exchangeVersion: ExchangeVersion; private credentials: Record; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { // this.integrationName = CALENDAR_INTEGRATIONS_TYPES.exchange; this.integrationName = "exchange2016_calendar"; diff --git a/packages/app-store/exchangecalendar/lib/CalendarService.ts b/packages/app-store/exchangecalendar/lib/CalendarService.ts index 1bb9b3210e..dd87692f92 100644 --- a/packages/app-store/exchangecalendar/lib/CalendarService.ts +++ b/packages/app-store/exchangecalendar/lib/CalendarService.ts @@ -1,5 +1,4 @@ import { XhrApi } from "@ewsjs/xhr"; -import { Credential } from "@prisma/client"; import { Appointment, Attendee, @@ -39,6 +38,7 @@ import { NewCalendarEventType, Person, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import { ExchangeAuthentication } from "../enums"; @@ -47,7 +47,7 @@ export default class ExchangeCalendarService implements Calendar { private log: typeof logger; private payload; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "exchange_calendar"; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); this.payload = JSON.parse( diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index a2d2cb543e..f6f8a1316d 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -1,4 +1,4 @@ -import { Credential, Prisma } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import { calendar_v3, google } from "googleapis"; import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; @@ -12,6 +12,7 @@ import type { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import { getGoogleAppKeys } from "./getGoogleAppKeys"; import { googleCredentialSchema } from "./googleCredentialSchema"; @@ -25,13 +26,13 @@ export default class GoogleCalendarService implements Calendar { private auth: { getToken: () => Promise }; private log: typeof logger; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "google_calendar"; this.auth = this.googleAuth(credential); this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); } - private googleAuth = (credential: Credential) => { + private googleAuth = (credential: CredentialPayload) => { const googleCredentials = googleCredentialSchema.parse(credential.key); async function getGoogleAuth() { diff --git a/packages/app-store/hubspotothercalendar/lib/CalendarService.ts b/packages/app-store/hubspotothercalendar/lib/CalendarService.ts index f02e1b7a6f..f8d3edc7bd 100644 --- a/packages/app-store/hubspotothercalendar/lib/CalendarService.ts +++ b/packages/app-store/hubspotothercalendar/lib/CalendarService.ts @@ -2,7 +2,6 @@ import * as hubspot from "@hubspot/api-client"; import { BatchInputPublicAssociation } from "@hubspot/api-client/lib/codegen/crm/associations"; import { PublicObjectSearchRequest } from "@hubspot/api-client/lib/codegen/crm/contacts"; import { SimplePublicObjectInput } from "@hubspot/api-client/lib/codegen/crm/objects/meetings"; -import { Credential } from "@prisma/client"; import { getLocation } from "@calcom/lib/CalEventParser"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -17,6 +16,7 @@ import type { NewCalendarEventType, Person, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import type { HubspotToken } from "../api/callback"; @@ -31,7 +31,7 @@ export default class HubspotOtherCalendarService implements Calendar { private client_id = ""; private client_secret = ""; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "hubspot_other_calendar"; this.auth = this.hubspotAuth(credential).then((r) => r); @@ -148,7 +148,7 @@ export default class HubspotOtherCalendarService implements Calendar { return hubspotClient.crm.objects.meetings.basicApi.archive(uid); }; - private hubspotAuth = async (credential: Credential) => { + private hubspotAuth = async (credential: CredentialPayload) => { const appKeys = await getAppKeysFromSlug("hubspot"); if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id; if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret; diff --git a/packages/app-store/huddle01video/lib/VideoApiAdapter.ts b/packages/app-store/huddle01video/lib/VideoApiAdapter.ts index 00760f30e2..e1900d8853 100644 --- a/packages/app-store/huddle01video/lib/VideoApiAdapter.ts +++ b/packages/app-store/huddle01video/lib/VideoApiAdapter.ts @@ -17,15 +17,17 @@ const Huddle01VideoApiAdapter = (): VideoApiAdapter => { "https://wpss2zlpb9.execute-api.us-east-1.amazonaws.com/new-meeting?utmCampaign=cal.com&utmSource=partner&utmMedium=calendar" ); - const json = await handleErrorsJson(res); + const json = await handleErrorsJson<{ url: string }>(res); const { url } = huddle01Schema.parse(json); - - return Promise.resolve({ - type: "huddle01_video", - id: randomString(21), - password: "", - url, - }); + if (url) { + return Promise.resolve({ + type: "huddle01_video", + id: randomString(21), + password: "", + url, + }); + } + return Promise.reject("Url was not received in response body."); }, deleteMeeting: async (): Promise => { Promise.resolve(); diff --git a/packages/app-store/larkcalendar/lib/CalendarService.ts b/packages/app-store/larkcalendar/lib/CalendarService.ts index 7bedd79f21..73ae413b11 100644 --- a/packages/app-store/larkcalendar/lib/CalendarService.ts +++ b/packages/app-store/larkcalendar/lib/CalendarService.ts @@ -1,5 +1,3 @@ -import { Credential } from "@prisma/client"; - import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; @@ -11,6 +9,7 @@ import type { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import { handleLarkError, isExpired, LARK_HOST } from "../common"; import type { @@ -36,13 +35,13 @@ export default class LarkCalendarService implements Calendar { private log: typeof logger; auth: { getToken: () => Promise }; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "lark_calendar"; this.auth = this.larkAuth(credential); this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); } - private larkAuth = (credential: Credential) => { + private larkAuth = (credential: CredentialPayload) => { const larkAuthCredentials = credential.key as LarkAuthCredentials; return { getToken: () => @@ -52,7 +51,7 @@ export default class LarkCalendarService implements Calendar { }; }; - private refreshAccessToken = async (credential: Credential) => { + private refreshAccessToken = async (credential: CredentialPayload) => { const larkAuthCredentials = credential.key as LarkAuthCredentials; const refreshExpireDate = larkAuthCredentials.refresh_expires_date; const refreshToken = larkAuthCredentials.refresh_token; diff --git a/packages/app-store/office365calendar/lib/CalendarService.ts b/packages/app-store/office365calendar/lib/CalendarService.ts index 688f333458..522a399bc2 100644 --- a/packages/app-store/office365calendar/lib/CalendarService.ts +++ b/packages/app-store/office365calendar/lib/CalendarService.ts @@ -1,5 +1,4 @@ import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta"; -import { Credential } from "@prisma/client"; import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors"; @@ -13,6 +12,7 @@ import type { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import { O365AuthCredentials } from "../types/Office365Calendar"; import { getOfficeAppKeys } from "./getOfficeAppKeys"; @@ -45,7 +45,7 @@ export default class Office365CalendarService implements Calendar { auth: { getToken: () => Promise }; private apiGraphUrl = "https://graph.microsoft.com/v1.0"; - constructor(credential: Credential) { + constructor(credential: CredentialPayload) { this.integrationName = "office365_calendar"; this.auth = this.o365Auth(credential); @@ -131,7 +131,7 @@ export default class Office365CalendarService implements Calendar { url: `/me/calendars/${calendarId}/calendarView${filter}`, })); const response = await this.apiGraphBatchCall(requests); - const responseBody = await handleErrorsJson(response); + const responseBody = await this.handleErrorJsonOffice365Calendar(response); let responseBatchApi: IBatchResponse = { responses: [] }; if (typeof responseBody === "string") { responseBatchApi = this.handleTextJsonResponseWithHtmlInBody(responseBody); @@ -158,12 +158,12 @@ export default class Office365CalendarService implements Calendar { async listCalendars(): Promise { const response = await this.fetcher(`/me/calendars`); - let responseBody = await handleErrorsJson(response); + let responseBody = await handleErrorsJson<{ value: OfficeCalendar[] }>(response); // If responseBody is valid then parse the JSON text if (typeof responseBody === "string") { responseBody = JSON.parse(responseBody) as { value: OfficeCalendar[] }; } - return responseBody.value.map((cal: OfficeCalendar) => { + return responseBody?.value.map((cal: OfficeCalendar) => { const calendar: IntegrationCalendar = { externalId: cal.id ?? "No Id", integration: this.integrationName, @@ -175,7 +175,7 @@ export default class Office365CalendarService implements Calendar { }); } - private o365Auth = (credential: Credential) => { + private o365Auth = (credential: CredentialPayload) => { const isExpired = (expiryDate: number) => expiryDate < Math.round(+new Date() / 1000); const o365AuthCredentials = credential.key as O365AuthCredentials; @@ -192,7 +192,7 @@ export default class Office365CalendarService implements Calendar { client_secret, }), }); - const responseBody = await handleErrorsJson(response); + const responseBody = await handleErrorsJson<{ access_token: string; expires_in: number }>(response); o365AuthCredentials.access_token = responseBody.access_token; o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); await prisma.credential.update({ @@ -283,7 +283,7 @@ export default class Office365CalendarService implements Calendar { } const newResponse = await this.apiGraphBatchCall(newLinkRequest); - let newResponseBody = await handleErrorsJson(newResponse); + let newResponseBody = await handleErrorsJson(newResponse); if (typeof newResponseBody === "string") { newResponseBody = this.handleTextJsonResponseWithHtmlInBody(newResponseBody); @@ -324,7 +324,7 @@ export default class Office365CalendarService implements Calendar { await new Promise((r) => setTimeout(r, retryAfterTimeout)); const newResponses = await this.apiGraphBatchCall(failedRequest); - let newResponseBody = await handleErrorsJson(newResponses); + let newResponseBody = await handleErrorsJson(newResponses); if (typeof newResponseBody === "string") { newResponseBody = this.handleTextJsonResponseWithHtmlInBody(newResponseBody); } @@ -393,4 +393,21 @@ export default class Office365CalendarService implements Calendar { [] ); }; + + private handleErrorJsonOffice365Calendar = (response: Response): Promise => { + if (response.headers.get("content-encoding") === "gzip") { + return response.text(); + } + + if (response.status === 204) { + return new Promise((resolve) => resolve({} as Type)); + } + + if (!response.ok && response.status < 200 && response.status >= 300) { + response.json().then(console.log); + throw Error(response.statusText); + } + + return response.json(); + }; } diff --git a/packages/app-store/office365video/lib/VideoApiAdapter.ts b/packages/app-store/office365video/lib/VideoApiAdapter.ts index eb6e7d61d9..6869f107ec 100644 --- a/packages/app-store/office365video/lib/VideoApiAdapter.ts +++ b/packages/app-store/office365video/lib/VideoApiAdapter.ts @@ -1,9 +1,10 @@ -import { Credential } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import type { PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -32,8 +33,19 @@ interface O365AuthCredentials { ext_expires_in: number; } +interface ITokenResponse { + expiry_date: number; + expires_in?: number; + token_type: string; + scope: string; + access_token: string; + refresh_token: string; + error?: string; + error_description?: string; +} + // Checks to see if our O365 user token is valid or if we need to refresh -const o365Auth = async (credential: Credential) => { +const o365Auth = async (credential: CredentialPayload) => { const appKeys = await getAppKeysFromSlug("msteams"); if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; @@ -55,13 +67,14 @@ const o365Auth = async (credential: Credential) => { client_secret, }), }); - const responseBody = await handleErrorsJson(response); - if (responseBody.error) { + + const responseBody = await handleErrorsJson(response); + if (responseBody?.error) { console.error(responseBody); throw new HttpError({ statusCode: 500, message: "Error contacting MS Teams" }); } // set expiry date as offset from current time. - responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000); + responseBody.expiry_date = Math.round(Date.now() + (responseBody?.expires_in || 0) * 1000); delete responseBody.expires_in; // Store new tokens in database. await prisma.credential.update({ @@ -69,7 +82,8 @@ const o365Auth = async (credential: Credential) => { id: credential.id, }, data: { - key: responseBody, + // @NOTE: prisma doesn't know key its a JSON so do as responseBody + key: responseBody as unknown as Prisma.InputJsonValue, }, }); o365AuthCredentials.expiry_date = responseBody.expiry_date; @@ -85,7 +99,7 @@ const o365Auth = async (credential: Credential) => { }; }; -const TeamsVideoApiAdapter = (credential: Credential): VideoApiAdapter => { +const TeamsVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => { const auth = o365Auth(credential); const translateEvent = (event: CalendarEvent) => { diff --git a/packages/app-store/tandemvideo/lib/VideoApiAdapter.ts b/packages/app-store/tandemvideo/lib/VideoApiAdapter.ts index 3b489f4e42..28ad87b21a 100644 --- a/packages/app-store/tandemvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/tandemvideo/lib/VideoApiAdapter.ts @@ -1,9 +1,10 @@ -import { Credential } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import type { PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -17,11 +18,25 @@ interface TandemToken { access_token: string; } +interface ITandemRefreshToken { + expires_in?: number; + expiry_date?: number; + access_token: string; + refresh_token: string; +} + +interface ITandemCreateMeetingResponse { + data: { + id: string; + event_link: string; + }; +} + let client_id = ""; let client_secret = ""; let base_url = ""; -const tandemAuth = async (credential: Credential) => { +const tandemAuth = async (credential: CredentialPayload) => { const appKeys = await getAppKeysFromSlug("tandem"); if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; @@ -33,34 +48,33 @@ const tandemAuth = async (credential: Credential) => { const credentialKey = credential.key as unknown as TandemToken; const isTokenValid = (token: TandemToken) => token && token.access_token && token.expiry_date < Date.now(); - const refreshAccessToken = (refreshToken: string) => { - fetch(`${base_url}/api/v1/oauth/v2/token`, { + const refreshAccessToken = async (refreshToken: string) => { + const result = await fetch(`${base_url}/api/v1/oauth/v2/token`, { method: "POST", body: new URLSearchParams({ client_id, client_secret, code: refreshToken, }), - }) - .then(handleErrorsJson) - .then(async (responseBody) => { - // set expiry date as offset from current time. - responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000); - delete responseBody.expires_in; - // Store new tokens in database. - await prisma.credential.update({ - where: { - id: credential.id, - }, - data: { - key: responseBody, - }, - }); - credentialKey.expiry_date = responseBody.expiry_date; - credentialKey.access_token = responseBody.access_token; - credentialKey.refresh_token = responseBody.refresh_token; - return credentialKey.access_token; - }); + }); + const responseBody = await handleErrorsJson(result); + + // set expiry date as offset from current time. + responseBody.expiry_date = Math.round(Date.now() + (responseBody.expires_in || 0) * 1000); + delete responseBody.expires_in; + // Store new tokens in database. + await prisma.credential.update({ + where: { + id: credential.id, + }, + data: { + key: responseBody as unknown as Prisma.InputJsonValue, + }, + }); + credentialKey.expiry_date = responseBody.expiry_date; + credentialKey.access_token = responseBody.access_token; + credentialKey.refresh_token = responseBody.refresh_token; + return credentialKey.access_token; }; return { @@ -71,7 +85,7 @@ const tandemAuth = async (credential: Credential) => { }; }; -const TandemVideoApiAdapter = (credential: Credential): VideoApiAdapter => { +const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => { const auth = tandemAuth(credential); const _parseDate = (date: string) => { @@ -91,7 +105,7 @@ const TandemVideoApiAdapter = (credential: Credential): VideoApiAdapter => { }); }; - const _translateResult = (result: { data: { id: string; event_link: string } }) => { + const _translateResult = (result: ITandemCreateMeetingResponse) => { return { type: "tandem_video", id: result.data.id as string, @@ -115,9 +129,10 @@ const TandemVideoApiAdapter = (credential: Credential): VideoApiAdapter => { "Content-Type": "application/json", }, body: _translateEvent(event, "meeting"), - }).then(handleErrorsJson); + }); + const responseBody = await handleErrorsJson(result); - return Promise.resolve(_translateResult(result)); + return Promise.resolve(_translateResult(responseBody)); }, deleteMeeting: async (uid: string): Promise => { @@ -143,9 +158,10 @@ const TandemVideoApiAdapter = (credential: Credential): VideoApiAdapter => { "Content-Type": "application/json", }, body: _translateEvent(event, "updates"), - }).then(handleErrorsJson); + }); + const responseBody = await handleErrorsJson(result); - return Promise.resolve(_translateResult(result)); + return Promise.resolve(_translateResult(responseBody)); }, }; }; diff --git a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts index 92a5e4229c..3d2bce6b68 100644 --- a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts @@ -1,11 +1,11 @@ -import { Credential } from "@prisma/client"; import { z } from "zod"; import dayjs from "@calcom/dayjs"; -import { handleErrorsJson } from "@calcom/lib/errors"; import prisma from "@calcom/prisma"; +import { Credential } from "@calcom/prisma/client"; import { Frequency } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import type { PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -69,7 +69,7 @@ const zoomRefreshedTokenSchema = z.object({ scope: z.string(), }); -const zoomAuth = (credential: Credential) => { +const zoomAuth = (credential: CredentialPayload) => { const refreshAccessToken = async (refreshToken: string) => { const { client_id, client_secret } = await getZoomAppKeys(); const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64"); @@ -86,8 +86,13 @@ const zoomAuth = (credential: Credential) => { }), }); - const responseBody = await handleZoomResponse(response); + const responseBody = await handleZoomResponse(response, credential.id); + if (responseBody.error) { + if (responseBody.error === "invalid_grant") { + return Promise.reject(new Error("Invalid grant for Cal.com zoom app")); + } + } // We check the if the new credentials matches the expected response structure const parsedToken = zoomRefreshedTokenSchema.safeParse(responseBody); @@ -138,7 +143,7 @@ type ZoomRecurrence = { monthly_day?: number; // 1-31 }; -const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => { +const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => { const translateEvent = (event: CalendarEvent) => { const getRecurrence = ({ recurringEvent, @@ -232,7 +237,7 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => { ...options?.headers, }, }); - const responseBody = await handleErrorsJson(response); + const responseBody = await handleZoomResponse(response, credential.id); return responseBody; }; @@ -241,6 +246,7 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => { try { // TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled. const responseBody = await fetchZoomApi("users/me/meetings?type=scheduled&page_size=300"); + const data = zoomMeetingsSchema.parse(responseBody); return data.meetings.map((meeting) => ({ start: meeting.start_time, @@ -254,13 +260,19 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => { }, createMeeting: async (event: CalendarEvent): Promise => { try { - const response: ZoomEventResult = await fetchZoomApi("users/me/meetings", { + const response = await fetchZoomApi("users/me/meetings", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(translateEvent(event)), }); + if (response.error) { + if (response.error === "invalid_grant") { + await invalidateCredential(credential.id); + return Promise.reject(new Error("Invalid grant for Cal.com zoom app")); + } + } const result = zoomEventResultSchema.parse(response); @@ -280,42 +292,73 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => { } }, deleteMeeting: async (uid: string): Promise => { - await fetchZoomApi(`meetings/${uid}`, { - method: "DELETE", - }); - - return Promise.resolve(); + try { + await fetchZoomApi(`meetings/${uid}`, { + method: "DELETE", + }); + return Promise.resolve(); + } catch (err) { + return Promise.reject(new Error("Failed to delete meeting")); + } }, updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise => { - await fetchZoomApi(`meetings/${bookingRef.uid}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(translateEvent(event)), - }); + try { + await fetchZoomApi(`meetings/${bookingRef.uid}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(translateEvent(event)), + }); - return Promise.resolve({ - type: "zoom_video", - id: bookingRef.meetingId as string, - password: bookingRef.meetingPassword as string, - url: bookingRef.meetingUrl as string, - }); + return Promise.resolve({ + type: "zoom_video", + id: bookingRef.meetingId as string, + password: bookingRef.meetingPassword as string, + url: bookingRef.meetingUrl as string, + }); + } catch (err) { + return Promise.reject(new Error("Failed to update meeting")); + } }, }; }; -const handleZoomResponse = async (response: Response) => { - if (response.headers.get("content-encoding") === "gzip") { +const handleZoomResponse = async (response: Response, credentialId: Credential["id"]) => { + let _response = response.clone(); + if (_response.headers.get("content-encoding") === "gzip") { const responseString = await response.text(); - return JSON.parse(responseString); + _response = JSON.parse(responseString); } - if (!response.ok && response.status < 200 && response.status >= 300) { - response.json().then(console.log); + if (!response.ok || (response.status < 200 && response.status >= 300)) { + const responseBody = await _response.json(); + + if ((response && response.status === 124) || responseBody.error === "invalid_grant") { + await invalidateCredential(credentialId); + } throw Error(response.statusText); } return response.json(); }; +const invalidateCredential = async (credentialId: Credential["id"]) => { + const credential = await prisma.credential.findUnique({ + where: { + id: credentialId, + }, + }); + + if (credential) { + await prisma.credential.update({ + where: { + id: credentialId, + }, + data: { + invalid: true, + }, + }); + } +}; + export default ZoomVideoApiAdapter; diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index ab0cc61876..58c18e5e68 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -1,21 +1,20 @@ -import { Credential, SelectedCalendar } from "@prisma/client"; +import { SelectedCalendar } from "@prisma/client"; import { createHash } from "crypto"; import _ from "lodash"; import cache from "memory-cache"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import getApps from "@calcom/app-store/utils"; -import type { ExtendedCredential } from "@calcom/core/EventManager"; import { getUid } from "@calcom/lib/CalEventParser"; import logger from "@calcom/lib/logger"; import { performance } from "@calcom/lib/server/perfObserver"; -import { App } from "@calcom/types/App"; import type { CalendarEvent, EventBusyDate, NewCalendarEventType } from "@calcom/types/Calendar"; +import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import type { EventResult } from "@calcom/types/EventManager"; const log = logger.getChildLogger({ prefix: ["CalendarManager"] }); -export const getCalendarCredentials = (credentials: Array) => { +export const getCalendarCredentials = (credentials: Array) => { const calendarCredentials = getApps(credentials) .filter((app) => app.type.endsWith("_calendar")) .flatMap((app) => { @@ -105,8 +104,8 @@ export const getConnectedCalendars = async ( */ const cleanIntegrationKeys = ( appIntegration: ReturnType[number]["integration"] & { - credentials?: Array; - credential: Credential; + credentials?: Array; + credential: CredentialPayload; } ) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -117,7 +116,7 @@ const cleanIntegrationKeys = ( const CACHING_TIME = 30_000; // 30 seconds const getCachedResults = async ( - withCredentials: Credential[], + withCredentials: CredentialPayload[], dateFrom: string, dateTo: string, selectedCalendars: SelectedCalendar[] @@ -170,7 +169,7 @@ const getCachedResults = async ( }; export const getBusyCalendarTimes = async ( - withCredentials: Credential[], + withCredentials: CredentialPayload[], dateFrom: string, dateTo: string, selectedCalendars: SelectedCalendar[] @@ -185,7 +184,7 @@ export const getBusyCalendarTimes = async ( }; export const createEvent = async ( - credential: ExtendedCredential, + credential: CredentialWithAppName, calEvent: CalendarEvent ): Promise> => { const uid: string = getUid(calEvent); @@ -228,7 +227,7 @@ export const createEvent = async ( }; export const updateEvent = async ( - credential: ExtendedCredential, + credential: CredentialWithAppName, calEvent: CalendarEvent, bookingRefUid: string | null, externalCalendarId: string | null @@ -266,7 +265,11 @@ export const updateEvent = async ( }; }; -export const deleteEvent = (credential: Credential, uid: string, event: CalendarEvent): Promise => { +export const deleteEvent = ( + credential: CredentialPayload, + uid: string, + event: CalendarEvent +): Promise => { const calendar = getCalendar(credential); if (calendar) { return calendar.deleteEvent(uid, event); diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index e80e15913e..4aff2e1db3 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -1,4 +1,4 @@ -import { Credential, DestinationCalendar } from "@prisma/client"; +import { DestinationCalendar } from "@prisma/client"; import merge from "lodash/merge"; import { v5 as uuidv5 } from "uuid"; import { z } from "zod"; @@ -9,6 +9,7 @@ import getApps from "@calcom/app-store/utils"; import prisma from "@calcom/prisma"; import { createdEventSchema } from "@calcom/prisma/zod-utils"; import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar"; +import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import type { Event } from "@calcom/types/Event"; import type { CreateUpdateResult, @@ -58,17 +59,15 @@ export const processLocation = (event: CalendarEvent): CalendarEvent => { }; type EventManagerUser = { - credentials: Credential[]; + credentials: CredentialPayload[]; destinationCalendar: DestinationCalendar | null; }; type createdEventSchema = z.infer; -export type ExtendedCredential = Credential & { appName: string }; - export default class EventManager { - calendarCredentials: ExtendedCredential[]; - videoCredentials: ExtendedCredential[]; + calendarCredentials: CredentialWithAppName[]; + videoCredentials: CredentialWithAppName[]; /** * Takes an array of credentials and initializes a new instance of the EventManager. @@ -359,7 +358,7 @@ export default class EventManager { * @private */ - private getVideoCredential(event: CalendarEvent): ExtendedCredential | undefined { + private getVideoCredential(event: CalendarEvent): CredentialWithAppName | undefined { if (!event.location) { return undefined; } @@ -373,7 +372,7 @@ export default class EventManager { .sort((a, b) => { return b.id - a.id; }) - .find((credential: Credential) => credential.type.includes(integrationName)); + .find((credential: CredentialPayload) => credential.type.includes(integrationName)); /** * This might happen if someone tries to use a location with a missing credential, so we fallback to Cal Video. diff --git a/packages/core/videoClient.ts b/packages/core/videoClient.ts index 03f4a84421..edec7f0553 100644 --- a/packages/core/videoClient.ts +++ b/packages/core/videoClient.ts @@ -1,14 +1,13 @@ -import { Credential } from "@prisma/client"; import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; import appStore from "@calcom/app-store"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; -import type { ExtendedCredential } from "@calcom/core/EventManager"; import { sendBrokenIntegrationEmail } from "@calcom/emails"; import { getUid } from "@calcom/lib/CalEventParser"; import logger from "@calcom/lib/logger"; import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar"; +import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoApiAdapterFactory, VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -17,7 +16,7 @@ const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] }); const translator = short(); // factory -const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] => +const getVideoAdapters = (withCredentials: CredentialPayload[]): VideoApiAdapter[] => withCredentials.reduce((acc, cred) => { const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`; const app = appStore[appName as keyof typeof appStore]; @@ -30,12 +29,12 @@ const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] => return acc; }, []); -const getBusyVideoTimes = (withCredentials: Credential[]) => +const getBusyVideoTimes = (withCredentials: CredentialPayload[]) => Promise.all(getVideoAdapters(withCredentials).map((c) => c?.getAvailability())).then((results) => results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[]) ); -const createMeeting = async (credential: ExtendedCredential, calEvent: CalendarEvent) => { +const createMeeting = async (credential: CredentialWithAppName, calEvent: CalendarEvent) => { const uid: string = getUid(calEvent); if (!credential) { @@ -82,7 +81,7 @@ const createMeeting = async (credential: ExtendedCredential, calEvent: CalendarE }; const updateMeeting = async ( - credential: ExtendedCredential, + credential: CredentialWithAppName, calEvent: CalendarEvent, bookingRef: PartialReference | null ): Promise> => { @@ -121,7 +120,7 @@ const updateMeeting = async ( }; }; -const deleteMeeting = (credential: Credential, uid: string): Promise => { +const deleteMeeting = (credential: CredentialPayload, uid: string): Promise => { if (credential) { const videoAdapter = getVideoAdapters([credential])[0]; // There are certain video apps with no video adapter defined. e.g. riverby,whereby diff --git a/packages/lib/CalendarService.ts b/packages/lib/CalendarService.ts index 7a36bfc111..366d09aaa6 100644 --- a/packages/lib/CalendarService.ts +++ b/packages/lib/CalendarService.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/triple-slash-reference */ /// -import { Credential, Prisma } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import ICAL from "ical.js"; import type { Attendee, DateArray, DurationObject, Person } from "ics"; import { createEvent } from "ics"; @@ -26,6 +26,7 @@ import type { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; +import { CredentialPayload } from "@calcom/types/Credential"; import { getLocation, getRichDescription } from "./CalEventParser"; import { symmetricDecrypt } from "./crypto"; @@ -68,7 +69,7 @@ export default abstract class BaseCalendarService implements Calendar { protected integrationName = ""; private log: typeof logger; - constructor(credential: Credential, integrationName: string, url?: string) { + constructor(credential: CredentialPayload, integrationName: string, url?: string) { this.integrationName = integrationName; const { diff --git a/packages/lib/errors.ts b/packages/lib/errors.ts index 02ccce210b..adfd8534b9 100644 --- a/packages/lib/errors.ts +++ b/packages/lib/errors.ts @@ -10,13 +10,16 @@ export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: numb return new Error(`Unhandled error of type '${typeof cause}''`); } -export function handleErrorsJson(response: Response) { +export async function handleErrorsJson(response: Response): Promise { if (response.headers.get("content-encoding") === "gzip") { - return response.text(); + const responseText = await response.text(); + return new Promise((resolve) => resolve(JSON.parse(responseText))); } + if (response.status === 204) { - return new Promise((resolve) => resolve({})); + return new Promise((resolve) => resolve({} as Type)); } + if (!response.ok && response.status < 200 && response.status >= 300) { response.json().then(console.log); throw Error(response.statusText); diff --git a/packages/prisma/migrations/20221017205314_add_invalid_field_credential_table/migration.sql b/packages/prisma/migrations/20221017205314_add_invalid_field_credential_table/migration.sql new file mode 100644 index 0000000000..553035a590 --- /dev/null +++ b/packages/prisma/migrations/20221017205314_add_invalid_field_credential_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Credential" ADD COLUMN "invalid" BOOLEAN; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index cd96601c1c..a7e08e7180 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -100,6 +100,7 @@ model Credential { // How to make it a required column? appId String? destinationCalendars DestinationCalendar[] + invalid Boolean? } enum UserPlan { diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 335ffd368a..0abdc083b5 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -58,6 +58,7 @@ async function getUserFromSession({ key: true, userId: true, appId: true, + invalid: true, }, orderBy: { id: "asc", diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index cdc73ac6dd..50ff7be931 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -772,11 +772,19 @@ const loggedInViewerRouter = createProtectedRouter() const { variant, exclude, onlyInstalled } = input; const { credentials } = user; let apps = getApps(credentials).map( - ({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => ({ - ...app, - credentialIds: credentials.filter((c) => c.type === app.type).map((c) => c.id), - }) + ({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => { + const credentialIds = credentials.filter((c) => c.type === app.type).map((c) => c.id); + const invalidCredentialIds = credentials + .filter((c) => c.type === app.type && c.invalid) + .map((c) => c.id); + return { + ...app, + credentialIds, + invalidCredentialIds, + }; + } ); + if (exclude) { // exclusion filter apps = apps.filter((item) => (exclude ? !exclude.includes(item.variant) : true)); diff --git a/packages/types/Credential.d.ts b/packages/types/Credential.d.ts new file mode 100644 index 0000000000..dc9b9d2963 --- /dev/null +++ b/packages/types/Credential.d.ts @@ -0,0 +1,18 @@ +import { Prisma } from ".prisma/client"; + +/* + * The logic on this it's just using Credential Type doesn't reflect that some fields can be + * null sometimes, so with this we should get correct type. + * Also there may be a better place to save this. + */ +export type CredentialPayload = Prisma.CredentialGetPayload<{ + select: { + id: true; + appId: true; + type: true; + userId: true; + key: true; + }; +}>; + +export type CredentialWithAppName = CredentialPayload & { appName: string }; diff --git a/packages/types/VideoApiAdapter.d.ts b/packages/types/VideoApiAdapter.d.ts index 81e21c5e23..d395a32bdf 100644 --- a/packages/types/VideoApiAdapter.d.ts +++ b/packages/types/VideoApiAdapter.d.ts @@ -1,6 +1,5 @@ -import type { Credential } from "@prisma/client"; - import type { EventBusyDate } from "./Calendar"; +import { CredentialPayload } from "./Credential"; export interface VideoCallData { type: string; @@ -22,4 +21,4 @@ export type VideoApiAdapter = } | undefined; -export type VideoApiAdapterFactory = (credential: Credential) => VideoApiAdapter; +export type VideoApiAdapterFactory = (credential: CredentialPayload) => VideoApiAdapter; diff --git a/packages/ui/v2/core/Shell.tsx b/packages/ui/v2/core/Shell.tsx index 33b53a3d84..4cb618360c 100644 --- a/packages/ui/v2/core/Shell.tsx +++ b/packages/ui/v2/core/Shell.tsx @@ -30,8 +30,8 @@ import Dropdown, { import { Icon } from "@calcom/ui/Icon"; import TimezoneChangeDialog from "@calcom/ui/TimezoneChangeDialog"; import Button from "@calcom/ui/v2/core/Button"; -import Tips from "@calcom/ui/v2/modules/tips/Tips"; import showToast from "@calcom/ui/v2/core/notifications"; +import Tips from "@calcom/ui/v2/modules/tips/Tips"; /* TODO: Get this from endpoint */ import pkg from "../../../../apps/web/package.json";