From 621067acd8d32e5b8bc9f827b138f0c1dc7e2ef3 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:36:35 +0300 Subject: [PATCH] fix: platform gcal connect reconnect credentials (#14513) * fix: enable gcal connect to reconnect revoked google credentials * feat: callback to react when check fails --- apps/api/v2/src/ee/gcal/gcal.controller.ts | 16 ++++++++++- apps/api/v2/src/ee/gcal/gcal.module.ts | 12 +++++++- .../credentials/credentials.repository.ts | 19 +++++++++++-- .../selected-calendars.repository.ts | 17 +++++++++-- .../atoms/gcal-connect/GcalConnect.tsx | 28 ++++++++++++++++++- packages/platform/atoms/hooks/useGcal.ts | 11 ++++++-- .../examples/base/src/pages/calendars.tsx | 2 +- 7 files changed, 94 insertions(+), 11 deletions(-) diff --git a/apps/api/v2/src/ee/gcal/gcal.controller.ts b/apps/api/v2/src/ee/gcal/gcal.controller.ts index df92565572..22d3e5d8f1 100644 --- a/apps/api/v2/src/ee/gcal/gcal.controller.ts +++ b/apps/api/v2/src/ee/gcal/gcal.controller.ts @@ -1,3 +1,4 @@ +import { CalendarsService } from "@/ee/calendars/services/calendars.service"; import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output"; import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output"; import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output"; @@ -41,6 +42,7 @@ const CALENDAR_SCOPES = [ "https://www.googleapis.com/auth/calendar.events", ]; +// Controller for the GCalConnect Atom @Controller({ path: "ee/gcal", version: "2", @@ -54,7 +56,8 @@ export class GcalController { private readonly tokensRepository: TokensRepository, private readonly selectedCalendarsRepository: SelectedCalendarsRepository, private readonly config: ConfigService, - private readonly gcalService: GCalService + private readonly gcalService: GCalService, + private readonly calendarsService: CalendarsService ) {} private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`; @@ -148,6 +151,17 @@ export class GcalController { throw new BadRequestException("Invalid google oauth credentials."); } + const { connectedCalendars } = await this.calendarsService.getCalendars(userId); + const googleCalendar = connectedCalendars.find( + (cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE + ); + if (!googleCalendar) { + throw new UnauthorizedException("Google Calendar not connected."); + } + if (googleCalendar.error?.message) { + throw new UnauthorizedException(googleCalendar.error?.message); + } + return { status: SUCCESS_STATUS }; } } diff --git a/apps/api/v2/src/ee/gcal/gcal.module.ts b/apps/api/v2/src/ee/gcal/gcal.module.ts index 0ee9370d47..e6c40d864a 100644 --- a/apps/api/v2/src/ee/gcal/gcal.module.ts +++ b/apps/api/v2/src/ee/gcal/gcal.module.ts @@ -1,3 +1,4 @@ +import { CalendarsService } from "@/ee/calendars/services/calendars.service"; import { GcalController } from "@/ee/gcal/gcal.controller"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { GCalService } from "@/modules/apps/services/gcal.service"; @@ -6,12 +7,21 @@ import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; @Module({ imports: [PrismaModule, TokensModule, OAuthClientModule], - providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService], + providers: [ + AppsRepository, + ConfigService, + CredentialsRepository, + SelectedCalendarsRepository, + GCalService, + CalendarsService, + UsersRepository, + ], controllers: [GcalController], }) export class GcalModule {} diff --git a/apps/api/v2/src/modules/credentials/credentials.repository.ts b/apps/api/v2/src/modules/credentials/credentials.repository.ts index 6b7a897876..329d75fba6 100644 --- a/apps/api/v2/src/modules/credentials/credentials.repository.ts +++ b/apps/api/v2/src/modules/credentials/credentials.repository.ts @@ -9,16 +9,29 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants"; export class CredentialsRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - createAppCredential(type: keyof typeof APPS_TYPE_ID_MAPPING, key: Prisma.InputJsonValue, userId: number) { - return this.dbWrite.prisma.credential.create({ - data: { + async createAppCredential( + type: keyof typeof APPS_TYPE_ID_MAPPING, + key: Prisma.InputJsonValue, + userId: number + ) { + const credential = await this.getByTypeAndUserId(type, userId); + return this.dbWrite.prisma.credential.upsert({ + create: { type, key, userId, appId: APPS_TYPE_ID_MAPPING[type], }, + update: { + key, + invalid: false, + }, + where: { + id: credential?.id, + }, }); } + getByTypeAndUserId(type: string, userId: number) { return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } }); } diff --git a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts index a0a220e48d..b56ae971c3 100644 --- a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts +++ b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts @@ -7,13 +7,26 @@ export class SelectedCalendarsRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} createSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) { - return this.dbWrite.prisma.selectedCalendar.create({ - data: { + return this.dbWrite.prisma.selectedCalendar.upsert({ + create: { userId, externalId, credentialId, integration, }, + update: { + userId, + externalId, + credentialId, + integration, + }, + where: { + userId_integration_externalId: { + userId, + integration, + externalId, + }, + }, }); } diff --git a/packages/platform/atoms/gcal-connect/GcalConnect.tsx b/packages/platform/atoms/gcal-connect/GcalConnect.tsx index aa2ae11f5b..bcde8acaa6 100644 --- a/packages/platform/atoms/gcal-connect/GcalConnect.tsx +++ b/packages/platform/atoms/gcal-connect/GcalConnect.tsx @@ -3,6 +3,7 @@ import type { FC } from "react"; import { Button } from "@calcom/ui"; import { useAtomsContext } from "../hooks/useAtomsContext"; +import type { OnCheckErroType } from "../hooks/useGcal"; import { useGcal } from "../hooks/useGcal"; import { AtomsWrapper } from "../src/components/atoms-wrapper"; import { cn } from "../src/lib/utils"; @@ -11,16 +12,41 @@ interface GcalConnectProps { className?: string; label?: string; alreadyConnectedLabel?: string; + onCheckError?: OnCheckErroType; } +/** + * Renders a button to connect or disconnect the Google Calendar of a user. + * @requires AccessToken - The user must be authenticated with an access token passed to CalProvider. + * @component + * @example + * ```tsx + * + * ``` + * + * + * @param {string} [label="Connect Google Calendar"] - The label for the connect button. Optional. + * @param {string} [alreadyConnectedLabel="Connected Google Calendar"] - The label for the already connected button. Optional. + * @param {string} [className] - Additional CSS class name for the button. Optional. + * @param {OnCheckErroType} [onCheckError] - A callback function to handle errors when checking the connection status. Optional. + * @returns {JSX.Element} The rendered component. + */ export const GcalConnect: FC = ({ label = "Connect Google Calendar", alreadyConnectedLabel = "Connected Google Calendar", className, + onCheckError, }) => { const { isAuth } = useAtomsContext(); - const { allowConnect, checked, redirectToGcalOAuth } = useGcal({ isAuth }); + const { allowConnect, checked, redirectToGcalOAuth } = useGcal({ + isAuth, + onCheckError, + }); if (!isAuth || !checked) return <>; diff --git a/packages/platform/atoms/hooks/useGcal.ts b/packages/platform/atoms/hooks/useGcal.ts index e54e94f44f..dbce8e31e6 100644 --- a/packages/platform/atoms/hooks/useGcal.ts +++ b/packages/platform/atoms/hooks/useGcal.ts @@ -1,12 +1,16 @@ import { useState, useEffect } from "react"; +import type { ApiErrorResponse } from "@calcom/platform-types"; + import http from "../lib/http"; +export type OnCheckErroType = (err: ApiErrorResponse) => void; export interface useGcalProps { isAuth: boolean; + onCheckError?: OnCheckErroType; } -export const useGcal = ({ isAuth }: useGcalProps) => { +export const useGcal = ({ isAuth, onCheckError }: useGcalProps) => { const [allowConnect, setAllowConnect] = useState(false); const [checked, setChecked] = useState(false); @@ -26,7 +30,10 @@ export const useGcal = ({ isAuth }: useGcalProps) => { http ?.get("/ee/gcal/check") .then(() => setAllowConnect(false)) - .catch(() => setAllowConnect(true)) + .catch((err) => { + setAllowConnect(true); + onCheckError?.(err as ApiErrorResponse); + }) .finally(() => setChecked(true)); } }, [isAuth]); diff --git a/packages/platform/examples/base/src/pages/calendars.tsx b/packages/platform/examples/base/src/pages/calendars.tsx index 0a273b3061..9ae93bdb0e 100644 --- a/packages/platform/examples/base/src/pages/calendars.tsx +++ b/packages/platform/examples/base/src/pages/calendars.tsx @@ -27,7 +27,7 @@ export default function Calendars(props: { calUsername: string; calEmail: string connectedCalendars.map((connectedCalendar) => (

{connectedCalendar.integration.name}

- {connectedCalendar.calendars.map((calendar) => ( + {connectedCalendar.calendars?.map((calendar) => (

{calendar.name}