From 62fea25eb911377eba042abbfc20b629f842eb2d Mon Sep 17 00:00:00 2001 From: alannnc Date: Tue, 4 Oct 2022 16:56:38 +0200 Subject: [PATCH] fix/connected calendar list different states (#4821) * Display error if trpc query failed, catch error on getting calendar and cleanup to don't leak credentials to client * Add calendar connection broke error * Edit style on alert and disconnecting button * Ts errors and added translation * Type fix * Key and text change for broken calendar connection Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Alex van Andel --- .../v2/settings/my-account/calendars.tsx | 44 ++++++++- apps/web/public/static/locales/en/common.json | 11 ++- packages/core/CalendarManager.ts | 93 +++++++++++++------ 3 files changed, 115 insertions(+), 33 deletions(-) diff --git a/apps/web/pages/v2/settings/my-account/calendars.tsx b/apps/web/pages/v2/settings/my-account/calendars.tsx index cd1e173cb3..84b64e5aa3 100644 --- a/apps/web/pages/v2/settings/my-account/calendars.tsx +++ b/apps/web/pages/v2/settings/my-account/calendars.tsx @@ -7,6 +7,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Icon } from "@calcom/ui"; +import { Alert } from "@calcom/ui/v2"; import Badge from "@calcom/ui/v2/core/Badge"; import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen"; import Meta from "@calcom/ui/v2/core/Meta"; @@ -23,7 +24,7 @@ import { CalendarSwitch } from "@components/v2/settings/CalendarSwitch"; const SkeletonLoader = () => { return ( -
+
@@ -89,7 +90,30 @@ const CalendarsView = () => { {data.connectedCalendars.map((item) => ( - {item.calendars && ( + {item.error && item.error.message && ( + + {/* @TODO: add a reconnect button, that calls add api and delete old credential */} + query.refetch()} + buttonProps={{ + className: "border border-gray-300 py-[2px]", + color: "secondary", + }} + /> + + } + /> + )} + {item?.error === undefined && item.calendars && (
{ @@ -154,6 +178,22 @@ const CalendarsView = () => { /> ); }} + error={() => { + return ( + + An error ocurred while fetching your Calendars. + query.refetch()}> + try again + + . + + } + severity="error" + /> + ); + }} /> ); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 919bb51d56..cfdfdc7295 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1253,9 +1253,9 @@ "create_first_api_key_description": "API keys allow other apps to communicate with Cal.com", "back_to_signin": "Back to sign in", "reset_link_sent": "Reset link sent", - "password_reset_email":"An email is on it’s way to {{email}} with instructions to reset your password.", - "password_reset_leading":"If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.", - "password_updated":"Password updated!", + "password_reset_email": "An email is on it’s way to {{email}} with instructions to reset your password.", + "password_reset_leading": "If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.", + "password_updated": "Password updated!", "pending_payment": "Pending payment", "confirmation_page_rainbow": "Token gate your event with tokens or NFTs on Ethereum, Polygon, and more.", "not_on_cal": "Not on Cal.com", @@ -1272,5 +1272,8 @@ "format": "Format", "uppercase_for_letters": "Use uppercase for all letters", "replace_whitespaces_underscores": "Replace whitespaces with underscores", - "ignore_special_characters": "Ignore special characters in your Additonal Input label. Use only letters and numbers" + "ignore_special_characters": "Ignore special characters in your Additional Input label. Use only letters and numbers", + "retry": "Retry", + "fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again or reach out to customer support.", + "calendar_connection_fail": "Calendar connection failed" } diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index e4db1f9b81..d47a536d81 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -8,6 +8,7 @@ import getApps from "@calcom/app-store/utils"; 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 type { EventResult } from "@calcom/types/EventManager"; @@ -33,47 +34,85 @@ export const getConnectedCalendars = async ( ) => { const connectedCalendars = await Promise.all( calendarCredentials.map(async (item) => { - const { calendar, integration, credential } = item; - const credentialId = credential.id; - if (!calendar) { + try { + const { calendar, integration, credential } = item; + + // Don't leak credentials to the client + const credentialId = credential.id; + if (!calendar) { + return { + integration, + credentialId, + }; + } + const cals = await calendar.listCalendars(); + const calendars = _(cals) + .map((cal) => ({ + ...cal, + readOnly: cal.readOnly || false, + primary: cal.primary || null, + isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId), + credentialId, + })) + .sortBy(["primary"]) + .value(); + const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined); + if (!primary) { + return { + integration, + credentialId, + error: { + message: "No primary calendar found", + }, + }; + } + return { - integration, + integration: cleanIntegrationKeys(integration), credentialId, + primary, + calendars, }; - } - const cals = await calendar.listCalendars(); - const calendars = _(cals) - .map((cal) => ({ - ...cal, - readOnly: cal.readOnly || false, - primary: cal.primary || null, - isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId), - credentialId, - })) - .sortBy(["primary"]) - .value(); - const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined); - if (!primary) { + } catch (error) { + let errorMessage = "Could not get connected calendars"; + + // Here you can expect for specific errors + if (error instanceof Error) { + if (error.message === "invalid_grant") { + errorMessage = "Access token expired or revoked"; + } + } + return { - integration, - credentialId, + integration: cleanIntegrationKeys(item.integration), + credentialId: item.credential.id, error: { - message: "No primary calendar found", + message: errorMessage, }, }; } - return { - integration, - credentialId, - primary, - calendars, - }; }) ); return connectedCalendars; }; +/** + * Important function to prevent leaking credentials to the client + * @param appIntegration + * @returns App + */ +const cleanIntegrationKeys = ( + appIntegration: ReturnType[number]["integration"] & { + credentials?: Array; + credential: Credential; + } +) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { credentials, credential, ...rest } = appIntegration; + return rest; +}; + const CACHING_TIME = 30_000; // 30 seconds const getCachedResults = async (