diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index 1aec90fbe0..cebf818d61 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -2,6 +2,7 @@ import type { Auth } from "googleapis"; import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; +import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar"; import { WEBAPP_URL_FOR_OAUTH, WEBAPP_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { HttpError } from "@calcom/lib/http-error"; @@ -105,23 +106,38 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { appId: "google-calendar", }, }); + + const selectedCalendarWhereUnique = { + userId: req.session.user.id, + externalId: primaryCal.id, + integration: "google_calendar", + }; + // Wrapping in a try/catch to reduce chance of race conditions- // also this improves performance for most of the happy-paths. try { await prisma.selectedCalendar.create({ data: { - userId: req.session.user.id, - externalId: primaryCal.id, credentialId: credential.id, - integration: "google_calendar", + ...selectedCalendarWhereUnique, }, }); } catch (error) { - await prisma.credential.delete({ where: { id: credential.id } }); let errorMessage = "something_went_wrong"; if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { + // it is possible a selectedCalendar was orphaned, in this situation- + // we want to recover by connecting the existing selectedCalendar to the new Credential. + if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) { + res.redirect( + getSafeRedirectUrl(state?.returnTo) ?? + getInstalledAppPath({ variant: "calendar", slug: "google-calendar" }) + ); + return; + } + // else errorMessage = "account_already_linked"; } + await prisma.credential.delete({ where: { id: credential.id } }); res.redirect( `${ getSafeRedirectUrl(state?.onErrorReturnTo) ?? diff --git a/packages/app-store/office365calendar/api/callback.ts b/packages/app-store/office365calendar/api/callback.ts index 05d283af70..6877144053 100644 --- a/packages/app-store/office365calendar/api/callback.ts +++ b/packages/app-store/office365calendar/api/callback.ts @@ -1,6 +1,7 @@ import type { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta"; import type { NextApiRequest, NextApiResponse } from "next"; +import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { handleErrorsJson } from "@calcom/lib/errors"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; @@ -119,23 +120,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) appId: "office365-calendar", }, }); + const selectedCalendarWhereUnique = { + userId: req.session?.user.id, + integration: "office365_calendar", + externalId: defaultCalendar.id, + }; // Wrapping in a try/catch to reduce chance of race conditions- // also this improves performance for most of the happy-paths. try { await prisma.selectedCalendar.create({ data: { - userId: req.session?.user.id, - integration: "office365_calendar", - externalId: defaultCalendar.id, + ...selectedCalendarWhereUnique, credentialId: credential.id, }, }); } catch (error) { - await prisma.credential.delete({ where: { id: credential.id } }); let errorMessage = "something_went_wrong"; if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { + // it is possible a selectedCalendar was orphaned, in this situation- + // we want to recover by connecting the existing selectedCalendar to the new Credential. + if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) { + res.redirect( + getSafeRedirectUrl(state?.returnTo) ?? + getInstalledAppPath({ variant: "calendar", slug: "office365-calendar" }) + ); + return; + } + // else errorMessage = "account_already_linked"; } + await prisma.credential.delete({ where: { id: credential.id } }); res.redirect( `${ getSafeRedirectUrl(state?.onErrorReturnTo) ?? @@ -146,8 +160,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } } - return res.redirect( + res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "calendar", slug: "office365-calendar" }) ); + return; } diff --git a/packages/app-store/zohocalendar/api/callback.ts b/packages/app-store/zohocalendar/api/callback.ts index fcc01265fc..2ffa8373cd 100644 --- a/packages/app-store/zohocalendar/api/callback.ts +++ b/packages/app-store/zohocalendar/api/callback.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { stringify } from "querystring"; +import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import logger from "@calcom/lib/logger"; @@ -86,23 +87,36 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { appId: config.slug, }, }); + const selectedCalendarWhereUnique = { + userId: req.session?.user.id, + integration: config.type, + externalId: primaryCalendar.uid, + }; // Wrapping in a try/catch to reduce chance of race conditions- // also this improves performance for most of the happy-paths. try { await prisma.selectedCalendar.create({ data: { - userId: req.session.user.id, - integration: config.type, - externalId: primaryCalendar.uid, + ...selectedCalendarWhereUnique, credentialId: credential.id, }, }); } catch (error) { - await prisma.credential.delete({ where: { id: credential.id } }); let errorMessage = "something_went_wrong"; if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { + // it is possible a selectedCalendar was orphaned, in this situation- + // we want to recover by connecting the existing selectedCalendar to the new Credential. + if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) { + res.redirect( + getSafeRedirectUrl(state?.returnTo) ?? + getInstalledAppPath({ variant: "calendar", slug: config.slug }) + ); + return; + } + // else errorMessage = "account_already_linked"; } + await prisma.credential.delete({ where: { id: credential.id } }); res.redirect( `${ getSafeRedirectUrl(state?.onErrorReturnTo) ?? diff --git a/packages/lib/connectedCalendar.ts b/packages/lib/connectedCalendar.ts new file mode 100644 index 0000000000..b25c23c544 --- /dev/null +++ b/packages/lib/connectedCalendar.ts @@ -0,0 +1,29 @@ +import prisma from "@calcom/prisma"; + +export async function renewSelectedCalendarCredentialId( + selectedCalendarWhereUnique: { + userId: number; + integration: string; + externalId: string; + }, + credentialId: number +): Promise /* True if renewed, false if not */ { + const selectedCalendar = await prisma.selectedCalendar.findFirst({ + where: { + ...selectedCalendarWhereUnique, + credentialId: null, + }, + }); + if (selectedCalendar) { + await prisma.selectedCalendar.update({ + where: { + userId_integration_externalId: selectedCalendarWhereUnique, + }, + data: { + credentialId: credentialId, + }, + }); + return true; + } + return false; +}