From 197b435b6f04d5e43bb318a1515353e40b7dd898 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Thu, 14 Sep 2023 12:53:58 -0400 Subject: [PATCH] fix: Duplicate calendar events (#11327) Co-authored-by: Shivam Kalra Co-authored-by: zomars Co-authored-by: Alan --- apps/web/pages/api/availability/calendar.ts | 5 +- packages/app-store/applecalendar/api/add.ts | 2 + .../basecamp3/trpc/projectMutation.handler.ts | 2 + .../basecamp3/trpc/projects.handler.ts | 2 + packages/app-store/caldavcalendar/api/add.ts | 2 + .../dailyvideo/lib/VideoApiAdapter.ts | 1 + .../app-store/exchange2013calendar/api/add.ts | 2 + .../app-store/exchange2016calendar/api/add.ts | 2 + .../exchangecalendar/api/_postAdd.ts | 2 +- .../googlecalendar/lib/CalendarService.ts | 92 +++++++++---------- .../larkcalendar/lib/CalendarService.ts | 41 ++++++--- .../office365calendar/lib/CalendarService.ts | 41 +++++++-- packages/app-store/server.ts | 9 +- packages/app-store/utils.ts | 11 +-- packages/core/CalendarManager.ts | 10 +- packages/core/EventManager.ts | 74 ++++++++------- packages/core/getBusyTimes.ts | 5 +- packages/core/getUserAvailability.ts | 5 +- packages/core/videoClient.ts | 15 +-- .../bookings/lib/handleCancelBooking.ts | 15 ++- .../features/bookings/lib/handleNewBooking.ts | 40 +++++--- .../ee/payments/api/paypal-webhook.ts | 5 +- packages/features/ee/payments/api/webhook.ts | 9 +- packages/lib/CalendarService.ts | 34 ++++--- packages/lib/defaultEvents.ts | 5 +- packages/lib/server/getUsersCredentials.ts | 11 +-- packages/prisma/selects/credential.ts | 20 ++++ .../connectedCalendars.handler.ts | 2 + .../deleteCredential.handler.ts | 12 +-- .../loggedInViewer/integrations.handler.ts | 18 +++- .../viewer/bookings/editLocation.handler.ts | 2 + .../trpc/server/routers/viewer/slots/util.ts | 9 +- .../teams/getMemberAvailability.handler.ts | 5 +- packages/types/Calendar.d.ts | 3 +- packages/types/Credential.d.ts | 12 +-- 35 files changed, 313 insertions(+), 212 deletions(-) diff --git a/apps/web/pages/api/availability/calendar.ts b/apps/web/pages/api/availability/calendar.ts index 5482c58e2e..ce084da3cc 100644 --- a/apps/web/pages/api/availability/calendar.ts +++ b/apps/web/pages/api/availability/calendar.ts @@ -5,6 +5,7 @@ import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/Cale import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import notEmpty from "@calcom/lib/notEmpty"; import prisma from "@calcom/prisma"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; const selectedCalendarSelectSchema = z.object({ integration: z.string(), @@ -25,7 +26,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) id: session.user.id, }, select: { - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, timeZone: true, id: true, selectedCalendars: true, diff --git a/packages/app-store/applecalendar/api/add.ts b/packages/app-store/applecalendar/api/add.ts index 3a3ee8f7e9..2e9fb2788f 100644 --- a/packages/app-store/applecalendar/api/add.ts +++ b/packages/app-store/applecalendar/api/add.ts @@ -16,6 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) id: req.session?.user?.id, }, select: { + email: true, id: true, }, }); @@ -36,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const dav = new CalendarService({ id: 0, ...data, + user: { email: user.email }, }); await dav?.listCalendars(); await prisma.credential.create({ diff --git a/packages/app-store/basecamp3/trpc/projectMutation.handler.ts b/packages/app-store/basecamp3/trpc/projectMutation.handler.ts index ac0fc8141b..6a9ffaeb9e 100644 --- a/packages/app-store/basecamp3/trpc/projectMutation.handler.ts +++ b/packages/app-store/basecamp3/trpc/projectMutation.handler.ts @@ -1,4 +1,5 @@ import type { PrismaClient } from "@calcom/prisma/client"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { TRPCError } from "@calcom/trpc/server"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -24,6 +25,7 @@ export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHand where: { userId: user?.id, }, + select: credentialForCalendarServiceSelect, }); if (!credential) { diff --git a/packages/app-store/basecamp3/trpc/projects.handler.ts b/packages/app-store/basecamp3/trpc/projects.handler.ts index 181f1d911a..c068dc1466 100644 --- a/packages/app-store/basecamp3/trpc/projects.handler.ts +++ b/packages/app-store/basecamp3/trpc/projects.handler.ts @@ -1,4 +1,5 @@ import type { PrismaClient } from "@calcom/prisma/client"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { TRPCError } from "@calcom/trpc/server"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -20,6 +21,7 @@ export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => { where: { userId: user?.id, }, + select: credentialForCalendarServiceSelect, }); if (!credential) { throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" }); diff --git a/packages/app-store/caldavcalendar/api/add.ts b/packages/app-store/caldavcalendar/api/add.ts index 660723ffa8..4dab77e74e 100644 --- a/packages/app-store/caldavcalendar/api/add.ts +++ b/packages/app-store/caldavcalendar/api/add.ts @@ -17,6 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, select: { id: true, + email: true, }, }); @@ -36,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const dav = new CalendarService({ id: 0, ...data, + user: { email: user.email }, }); await dav?.listCalendars(); await prisma.credential.create({ diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts index 31d447ed7c..29a376732e 100644 --- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts @@ -60,6 +60,7 @@ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = { type: "daily_video", key: { apikey: process.env.DAILY_API_KEY }, userId: 0, + user: { email: "" }, appId: "daily-video", invalid: false, teamId: null, diff --git a/packages/app-store/exchange2013calendar/api/add.ts b/packages/app-store/exchange2013calendar/api/add.ts index 644103c135..f41a3f8a92 100644 --- a/packages/app-store/exchange2013calendar/api/add.ts +++ b/packages/app-store/exchange2013calendar/api/add.ts @@ -26,6 +26,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { }, select: { id: true, + email: true, }, }); @@ -42,6 +43,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { const dav = new CalendarService({ id: 0, ...data, + user: { email: user.email }, }); await dav?.listCalendars(); await prisma.credential.create({ diff --git a/packages/app-store/exchange2016calendar/api/add.ts b/packages/app-store/exchange2016calendar/api/add.ts index 1e04e1cd55..c8509a1ee7 100644 --- a/packages/app-store/exchange2016calendar/api/add.ts +++ b/packages/app-store/exchange2016calendar/api/add.ts @@ -26,6 +26,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { }, select: { id: true, + email: true, }, }); @@ -41,6 +42,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { try { const dav = new CalendarService({ id: 0, + user: { email: user.email }, ...data, }); await dav?.listCalendars(); diff --git a/packages/app-store/exchangecalendar/api/_postAdd.ts b/packages/app-store/exchangecalendar/api/_postAdd.ts index a408a29809..dbea3db769 100644 --- a/packages/app-store/exchangecalendar/api/_postAdd.ts +++ b/packages/app-store/exchangecalendar/api/_postAdd.ts @@ -36,7 +36,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) { }; try { - const service = new CalendarService({ id: 0, ...data }); + const service = new CalendarService({ id: 0, user: { email: session.user.email || "" }, ...data }); await service?.listCalendars(); await prisma.credential.create({ data }); } catch (reason) { diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 23d10c8dbe..32796ae948 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -28,11 +28,13 @@ export default class GoogleCalendarService implements Calendar { private integrationName = ""; private auth: { getToken: () => Promise }; private log: typeof logger; + private credential: CredentialPayload; constructor(credential: CredentialPayload) { this.integrationName = "google_calendar"; this.auth = this.googleAuth(credential); this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + this.credential = credential; } private googleAuth = (credential: CredentialPayload) => { @@ -84,22 +86,50 @@ export default class GoogleCalendarService implements Calendar { }; }; - async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise { - const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({ + private getAttendees = (event: CalendarEvent) => { + // When rescheduling events we know the external id of the calendar so we can just look for it in the destinationCalendar array. + const selectedHostDestinationCalendar = event.destinationCalendar?.find( + (cal) => cal.credentialId === this.credential.id + ); + const eventAttendees = event.attendees.map(({ id: _id, ...rest }) => ({ ...rest, responseStatus: "accepted", })); - // TODO: Check every other CalendarService for team members - const teamMembers = - calEventRaw.team?.members.map((m) => ({ - email: m.email, - displayName: m.name, + const attendees: calendar_v3.Schema$EventAttendee[] = [ + { + ...event.organizer, + id: String(event.organizer.id), responseStatus: "accepted", - })) || []; + organizer: true, + // Tried changing the display name to the user but GCal will not let you do that. It will only display the name of the external calendar. Leaving this in just incase it works in the future. + displayName: event.organizer.name, + email: selectedHostDestinationCalendar?.externalId ?? event.organizer.email, + }, + ...eventAttendees, + ]; + + if (event.team?.members) { + // TODO: Check every other CalendarService for team members + const teamAttendeesWithoutCurrentUser = event.team.members + .filter((member) => member.email !== this.credential.user?.email) + .map((m) => { + const teamMemberDestinationCalendar = event.destinationCalendar?.find( + (calendar) => calendar.integration === "google_calendar" && calendar.userId === m.id + ); + return { + email: teamMemberDestinationCalendar?.externalId ?? m.email, + displayName: m.name, + responseStatus: "accepted", + }; + }); + attendees.push(...teamAttendeesWithoutCurrentUser); + } + + return attendees; + }; + + async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise { return new Promise(async (resolve, reject) => { - const selectedHostDestinationCalendar = calEventRaw.destinationCalendar?.find( - (cal) => cal.credentialId === credentialId - ); const myGoogleAuth = await this.auth.getToken(); const payload: calendar_v3.Schema$Event = { summary: calEventRaw.title, @@ -112,19 +142,7 @@ export default class GoogleCalendarService implements Calendar { dateTime: calEventRaw.endTime, timeZone: calEventRaw.organizer.timeZone, }, - attendees: [ - { - ...calEventRaw.organizer, - id: String(calEventRaw.organizer.id), - responseStatus: "accepted", - organizer: true, - email: selectedHostDestinationCalendar?.externalId - ? selectedHostDestinationCalendar.externalId - : calEventRaw.organizer.email, - }, - ...eventAttendees, - ...teamMembers, - ], + attendees: this.getAttendees(calEventRaw), reminders: { useDefault: true, }, @@ -194,19 +212,7 @@ export default class GoogleCalendarService implements Calendar { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise { return new Promise(async (resolve, reject) => { - const [mainHostDestinationCalendar] = - event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : []; const myGoogleAuth = await this.auth.getToken(); - const eventAttendees = event.attendees.map(({ ...rest }) => ({ - ...rest, - responseStatus: "accepted", - })); - const teamMembers = - event.team?.members.map((m) => ({ - email: m.email, - displayName: m.name, - responseStatus: "accepted", - })) || []; const payload: calendar_v3.Schema$Event = { summary: event.title, description: getRichDescription(event), @@ -218,19 +224,7 @@ export default class GoogleCalendarService implements Calendar { dateTime: event.endTime, timeZone: event.organizer.timeZone, }, - attendees: [ - { - ...event.organizer, - id: String(event.organizer.id), - organizer: true, - responseStatus: "accepted", - email: mainHostDestinationCalendar?.externalId - ? mainHostDestinationCalendar.externalId - : event.organizer.email, - }, - ...(eventAttendees as any), - ...(teamMembers as any), - ], + attendees: this.getAttendees(event), reminders: { useDefault: true, }, diff --git a/packages/app-store/larkcalendar/lib/CalendarService.ts b/packages/app-store/larkcalendar/lib/CalendarService.ts index f2979b5952..88dba3fc25 100644 --- a/packages/app-store/larkcalendar/lib/CalendarService.ts +++ b/packages/app-store/larkcalendar/lib/CalendarService.ts @@ -34,11 +34,13 @@ export default class LarkCalendarService implements Calendar { private integrationName = ""; private log: typeof logger; auth: { getToken: () => Promise }; + private credential: CredentialPayload; constructor(credential: CredentialPayload) { this.integrationName = "lark_calendar"; this.auth = this.larkAuth(credential); this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + this.credential = credential; } private larkAuth = (credential: CredentialPayload) => { @@ -122,10 +124,13 @@ export default class LarkCalendarService implements Calendar { }); }; - async createEvent(event: CalendarEvent): Promise { + async createEvent(event: CalendarEvent, credentialId: number): Promise { let eventId = ""; let eventRespData; - const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const mainHostDestinationCalendar = event.destinationCalendar + ? event.destinationCalendar.find((cal) => cal.credentialId === this.credential.id) ?? + event.destinationCalendar[0] + : undefined; const calendarId = mainHostDestinationCalendar?.externalId; if (!calendarId) { throw new Error("no calendar id"); @@ -143,7 +148,7 @@ export default class LarkCalendarService implements Calendar { } try { - await this.createAttendees(event, eventId); + await this.createAttendees(event, eventId, credentialId); return { ...eventRespData, uid: eventRespData.data.event.event_id as string, @@ -160,8 +165,11 @@ export default class LarkCalendarService implements Calendar { } } - private createAttendees = async (event: CalendarEvent, eventId: string) => { - const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + private createAttendees = async (event: CalendarEvent, eventId: string, credentialId: number) => { + const mainHostDestinationCalendar = event.destinationCalendar + ? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ?? + event.destinationCalendar[0] + : undefined; const calendarId = mainHostDestinationCalendar?.externalId; if (!calendarId) { this.log.error("no calendar id provided in createAttendees"); @@ -189,7 +197,9 @@ export default class LarkCalendarService implements Calendar { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) { const eventId = uid; let eventRespData; - const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const mainHostDestinationCalendar = event.destinationCalendar?.find( + (cal) => cal.externalId === externalCalendarId + ); const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId; if (!calendarId) { this.log.error("no calendar id provided in updateEvent"); @@ -234,7 +244,9 @@ export default class LarkCalendarService implements Calendar { * @returns */ async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) { - const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const mainHostDestinationCalendar = event.destinationCalendar?.find( + (cal) => cal.externalId === externalCalendarId + ); const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId; if (!calendarId) { this.log.error("no calendar id provided in deleteEvent"); @@ -393,13 +405,16 @@ export default class LarkCalendarService implements Calendar { attendeeArray.push(attendee); }); event.team?.members.forEach((member) => { - const attendee: LarkEventAttendee = { - type: "third_party", - is_optional: false, - third_party_email: member.email, - }; - attendeeArray.push(attendee); + if (member.email !== this.credential.user?.email) { + const attendee: LarkEventAttendee = { + type: "third_party", + is_optional: false, + third_party_email: member.email, + }; + attendeeArray.push(attendee); + } }); + return attendeeArray; }; } diff --git a/packages/app-store/office365calendar/lib/CalendarService.ts b/packages/app-store/office365calendar/lib/CalendarService.ts index ff28882c83..39e6ceb985 100644 --- a/packages/app-store/office365calendar/lib/CalendarService.ts +++ b/packages/app-store/office365calendar/lib/CalendarService.ts @@ -61,16 +61,20 @@ export default class Office365CalendarService implements Calendar { private accessToken: string | null = null; auth: { getToken: () => Promise }; private apiGraphUrl = "https://graph.microsoft.com/v1.0"; + private credential: CredentialPayload; constructor(credential: CredentialPayload) { this.integrationName = "office365_calendar"; this.auth = this.o365Auth(credential); - + this.credential = credential; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); } - async createEvent(event: CalendarEvent): Promise { - const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + async createEvent(event: CalendarEvent, credentialId: number): Promise { + const mainHostDestinationCalendar = event.destinationCalendar + ? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ?? + event.destinationCalendar[0] + : undefined; try { const eventsUrl = mainHostDestinationCalendar?.externalId ? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events` @@ -295,6 +299,16 @@ export default class Office365CalendarService implements Calendar { timeZone: event.organizer.timeZone, }, attendees: [ + // Add the calEvent organizer + { + emailAddress: { + address: event.destinationCalendar + ? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ?? + event.organizer.email + : event.organizer.email, + name: event.organizer.name, + }, + }, ...event.attendees.map((attendee) => ({ emailAddress: { address: attendee.email, @@ -303,13 +317,20 @@ export default class Office365CalendarService implements Calendar { type: "required", })), ...(event.team?.members - ? event.team?.members.map((member) => ({ - emailAddress: { - address: member.email, - name: member.name, - }, - type: "required", - })) + ? event.team?.members + .filter((member) => member.email !== this.credential.user?.email) + .map((member) => { + const destinationCalendar = + event.destinationCalendar && + event.destinationCalendar.find((cal) => cal.userId === member.id); + return { + emailAddress: { + address: destinationCalendar?.externalId ?? member.email, + name: member.name, + }, + type: "required", + }; + }) : []), ], location: event.location ? { displayName: getLocation(event) } : undefined, diff --git a/packages/app-store/server.ts b/packages/app-store/server.ts index 0d901131ea..0ff0c47e00 100644 --- a/packages/app-store/server.ts +++ b/packages/app-store/server.ts @@ -5,6 +5,7 @@ import { defaultVideoAppCategories } from "@calcom/app-store/utils"; import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials"; import { prisma } from "@calcom/prisma"; import { AppCategories } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { defaultLocations } from "./locations"; @@ -62,13 +63,7 @@ export async function getLocationGroupedOptions( }, }, select: { - id: true, - type: true, - key: true, - userId: true, - teamId: true, - appId: true, - invalid: true, + ...credentialForCalendarServiceSelect, team: { select: { name: true, diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 81c0e07b5c..4ceeb6aae3 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -1,11 +1,11 @@ import type { AppCategories } from "@prisma/client"; -import { Prisma } from "@prisma/client"; // If you import this file on any app it should produce circular dependency // import appStore from "./index"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import type { EventLocationType } from "@calcom/app-store/locations"; import type { App, AppMeta } from "@calcom/types/App"; +import type { CredentialPayload } from "@calcom/types/Credential"; export * from "./_utils/getEventTypeAppData"; @@ -30,13 +30,7 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => { return store; }, {} as Record); -const credentialData = Prisma.validator()({ - select: { id: true, type: true, key: true, userId: true, teamId: true, appId: true, invalid: true }, -}); - -export type CredentialData = Prisma.CredentialGetPayload; - -export type CredentialDataWithTeamName = CredentialData & { +export type CredentialDataWithTeamName = CredentialPayload & { team?: { name: string; } | null; @@ -64,6 +58,7 @@ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion key: appMeta.key!, userId: 0, + user: { email: "" }, teamId: null, appId: appMeta.slug, invalid: false, diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index e3a995dbe6..8c55c17651 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -14,7 +14,7 @@ import type { IntegrationCalendar, NewCalendarEventType, } from "@calcom/types/Calendar"; -import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; +import type { CredentialPayload } from "@calcom/types/Credential"; import type { EventResult } from "@calcom/types/EventManager"; import getCalendarsEvents from "./getCalendarsEvents"; @@ -216,7 +216,7 @@ export const getBusyCalendarTimes = async ( }; export const createEvent = async ( - credential: CredentialWithAppName, + credential: CredentialPayload, calEvent: CalendarEvent, externalId?: string ): Promise> => { @@ -255,7 +255,7 @@ export const createEvent = async ( : undefined; return { - appName: credential.appName, + appName: credential.appId || "", type: credential.type, success, uid, @@ -270,7 +270,7 @@ export const createEvent = async ( }; export const updateEvent = async ( - credential: CredentialWithAppName, + credential: CredentialPayload, calEvent: CalendarEvent, bookingRefUid: string | null, externalCalendarId: string | null @@ -311,7 +311,7 @@ export const updateEvent = async ( } return { - appName: credential.appName, + appName: credential.appId || "", type: credential.type, success, uid, diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index b244278efb..70e56c0095 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -1,4 +1,4 @@ -import type { DestinationCalendar, Booking } from "@prisma/client"; +import type { Booking, DestinationCalendar } from "@prisma/client"; // eslint-disable-next-line no-restricted-imports import { cloneDeep, merge } from "lodash"; import { v5 as uuidv5 } from "uuid"; @@ -7,18 +7,21 @@ import type { z } from "zod"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvideo/zod"; -import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; -import { MeetLocationType } from "@calcom/app-store/locations"; +import { getEventLocationTypeFromApp, MeetLocationType } from "@calcom/app-store/locations"; import getApps from "@calcom/app-store/utils"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { createdEventSchema } from "@calcom/prisma/zod-utils"; -import type { NewCalendarEventType } from "@calcom/types/Calendar"; -import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; -import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; +import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; import type { Event } from "@calcom/types/Event"; -import type { EventResult } from "@calcom/types/EventManager"; -import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager"; +import type { + CreateUpdateResult, + EventResult, + PartialBooking, + PartialReference, +} from "@calcom/types/EventManager"; import { createEvent, updateEvent } from "./CalendarManager"; import { createMeeting, updateMeeting } from "./videoClient"; @@ -68,8 +71,8 @@ export type EventManagerUser = { type createdEventSchema = z.infer; export default class EventManager { - calendarCredentials: CredentialWithAppName[]; - videoCredentials: CredentialWithAppName[]; + calendarCredentials: CredentialPayload[]; + videoCredentials: CredentialPayload[]; /** * Takes an array of credentials and initializes a new instance of the EventManager. @@ -338,26 +341,37 @@ export default class EventManager { private async createAllCalendarEvents(event: CalendarEvent) { let createdEvents: EventResult[] = []; if (event.destinationCalendar && event.destinationCalendar.length > 0) { - for (const destination of event.destinationCalendar) { + // Since GCal pushes events to multiple calendars we only want to create one event per booking + let gCalAdded = false; + const destinationCalendars: DestinationCalendar[] = event.destinationCalendar.reduce( + (destinationCals, cal) => { + if (cal.integration === "google_calendar") { + if (gCalAdded) { + return destinationCals; + } else { + gCalAdded = true; + destinationCals.push(cal); + } + } else { + destinationCals.push(cal); + } + return destinationCals; + }, + [] as DestinationCalendar[] + ); + for (const destination of destinationCalendars) { if (destination.credentialId) { let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId); if (!credential) { // Fetch credential from DB const credentialFromDB = await prisma.credential.findUnique({ - include: { - app: { - select: { - slug: true, - }, - }, - }, where: { id: destination.credentialId, }, + select: credentialForCalendarServiceSelect, }); - if (credentialFromDB && credentialFromDB.app?.slug) { + if (credentialFromDB && credentialFromDB.appId) { credential = { - appName: credentialFromDB?.app.slug ?? "", id: credentialFromDB.id, type: credentialFromDB.type, key: credentialFromDB.key, @@ -365,6 +379,7 @@ export default class EventManager { teamId: credentialFromDB.teamId, invalid: credentialFromDB.invalid, appId: credentialFromDB.appId, + user: credentialFromDB.user, }; } } @@ -416,7 +431,7 @@ export default class EventManager { * @private */ - private getVideoCredential(event: CalendarEvent): CredentialWithAppName | undefined { + private getVideoCredential(event: CalendarEvent): CredentialPayload | undefined { if (!event.location) { return undefined; } @@ -444,7 +459,7 @@ export default class EventManager { event.location + " because credential is missing for the app" ); - videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" }; + videoCredential = { ...FAKE_DAILY_CREDENTIAL }; } return videoCredential; @@ -524,20 +539,13 @@ export default class EventManager { if (!credential) { // Fetch credential from DB const credentialFromDB = await prisma.credential.findUnique({ - include: { - app: { - select: { - slug: true, - }, - }, - }, where: { id: reference.credentialId, }, + select: credentialForCalendarServiceSelect, }); - if (credentialFromDB && credentialFromDB.app?.slug) { + if (credentialFromDB && credentialFromDB.appId) { credential = { - appName: credentialFromDB?.app.slug ?? "", id: credentialFromDB.id, type: credentialFromDB.type, key: credentialFromDB.key, @@ -545,6 +553,7 @@ export default class EventManager { teamId: credentialFromDB.teamId, invalid: credentialFromDB.invalid, appId: credentialFromDB.appId, + user: credentialFromDB.user, }; } } @@ -567,6 +576,7 @@ export default class EventManager { where: { id: oldCalendarEvent.credentialId, }, + select: credentialForCalendarServiceSelect, }); const calendar = await getCalendar(calendarCredential); await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId); @@ -582,7 +592,7 @@ export default class EventManager { if (!calendarReference) { return { - appName: cred.appName, + appName: cred.appId || "", type: cred.type, success: false, uid: "", diff --git a/packages/core/getBusyTimes.ts b/packages/core/getBusyTimes.ts index e9b8db1ce2..c9e484a11e 100644 --- a/packages/core/getBusyTimes.ts +++ b/packages/core/getBusyTimes.ts @@ -1,4 +1,4 @@ -import type { Booking, Credential, EventType } from "@prisma/client"; +import type { Booking, EventType } from "@prisma/client"; import { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; import dayjs from "@calcom/dayjs"; @@ -9,9 +9,10 @@ import prisma from "@calcom/prisma"; import type { SelectedCalendar } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; import type { EventBusyDetails } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; export async function getBusyTimes(params: { - credentials: Credential[]; + credentials: CredentialPayload[]; userId: number; userEmail: string; username: string; diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 3675d9b2f0..c683a3aa04 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -14,6 +14,7 @@ import { performance } from "@calcom/lib/server/perfObserver"; import { getTotalBookingDuration } from "@calcom/lib/server/queries"; import prisma, { availabilityUserSelect } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils"; import type { EventBusyDate, @@ -81,7 +82,9 @@ const getUser = (where: Prisma.UserWhereInput) => where, select: { ...availabilityUserSelect, - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, }, }); diff --git a/packages/core/videoClient.ts b/packages/core/videoClient.ts index 1317b19fbb..7b48900d7d 100644 --- a/packages/core/videoClient.ts +++ b/packages/core/videoClient.ts @@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger"; import { prisma } from "@calcom/prisma"; import type { GetRecordingsResponseSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar"; -import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; +import type { CredentialPayload } from "@calcom/types/Credential"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoApiAdapterFactory, VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -48,7 +48,7 @@ const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) => results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[]) ); -const createMeeting = async (credential: CredentialWithAppName, calEvent: CalendarEvent) => { +const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => { const uid: string = getUid(calEvent); if (!credential || !credential.appId) { @@ -69,7 +69,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend createdEvent: VideoCallData | undefined; credentialId: number; } = { - appName: credential.appName, + appName: credential.appId || "", type: credential.type, uid, originalEvent: calEvent, @@ -110,7 +110,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend }; const updateMeeting = async ( - credential: CredentialWithAppName, + credential: CredentialPayload, calEvent: CalendarEvent, bookingRef: PartialReference | null ): Promise> => { @@ -131,7 +131,7 @@ const updateMeeting = async ( if (!updatedMeeting) { return { - appName: credential.appName, + appName: credential.appId || "", type: credential.type, success, uid, @@ -140,7 +140,7 @@ const updateMeeting = async ( } return { - appName: credential.appName, + appName: credential.appId || "", type: credential.type, success, uid, @@ -176,6 +176,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => { appId: "daily-video", type: "daily_video", userId: null, + user: { email: "" }, teamId: null, key: dailyAppKeys, invalid: false, @@ -200,6 +201,7 @@ const getRecordingsOfCalVideoByRoomName = async ( appId: "daily-video", type: "daily_video", userId: null, + user: { email: "" }, teamId: null, key: dailyAppKeys, invalid: false, @@ -222,6 +224,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (recordingId: string) => { appId: "daily-video", type: "daily_video", userId: null, + user: { email: "" }, teamId: null, key: dailyAppKeys, invalid: false, diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index e5acb48fad..9ae7959a8d 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -26,7 +26,8 @@ import { handleRefundError } from "@calcom/lib/payment/handleRefundError"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import { BookingStatus, MembershipRole, WorkflowMethods, WebhookTriggerEvents } from "@calcom/prisma/enums"; +import { BookingStatus, MembershipRole, WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -44,7 +45,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine user: { select: { id: true, - credentials: true, // Not leaking at the moment, be careful with + credentials: { select: credentialForCalendarServiceSelect }, // Not leaking at the moment, be careful with email: true, timeZone: true, timeFormat: true, @@ -434,6 +435,7 @@ async function handler(req: CustomRequest) { where: { id: credentialId, }, + select: credentialForCalendarServiceSelect, }); if (foundCalendarCredential) { calendarCredential = foundCalendarCredential; @@ -725,6 +727,7 @@ async function handleSeatedEventCancellation( where: { id: reference.credentialId, }, + select: credentialForCalendarServiceSelect, }); if (credential) { @@ -733,13 +736,7 @@ async function handleSeatedEventCancellation( attendees: evt.attendees.filter((evtAttendee) => attendee.email !== evtAttendee.email), }; if (reference.type.includes("_video")) { - integrationsToUpdate.push( - updateMeeting( - { ...credential, appName: evt.location?.replace("integrations:", "") || "" }, - updatedEvt, - reference - ) - ); + integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference)); } if (reference.type.includes("_calendar")) { const calendar = await getCalendar(credential); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 11194e8b80..732409696c 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1,4 +1,4 @@ -import type { App, Attendee, Credential, EventTypeCustomInput, DestinationCalendar } from "@prisma/client"; +import type { App, Attendee, DestinationCalendar, EventTypeCustomInput } from "@prisma/client"; import { Prisma } from "@prisma/client"; import async from "async"; import { isValidPhoneNumber } from "libphonenumber-js"; @@ -69,6 +69,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma, { userSelect } from "@calcom/prisma"; import type { BookingReference } from "@calcom/prisma/client"; import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { bookingCreateBodySchemaForApi, bookingCreateSchemaLegacyPropsForApi, @@ -85,6 +86,7 @@ import type { IntervalLimit, Person, } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { EventTypeInfo } from "../../webhooks/lib/sendPayload"; @@ -111,11 +113,12 @@ interface IEventTypePaymentCredentialType { * * @param credential */ -async function refreshCredential(credential: Credential): Promise { +async function refreshCredential(credential: CredentialPayload): Promise { const newCredential = await prisma.credential.findUnique({ where: { id: credential.id, }, + select: credentialForCalendarServiceSelect, }); if (!newCredential) { @@ -130,7 +133,7 @@ async function refreshCredential(credential: Credential): Promise { * * @param credentials */ -async function refreshCredentials(credentials: Array): Promise> { +async function refreshCredentials(credentials: Array): Promise> { return await async.mapLimit(credentials, 5, refreshCredential); } @@ -139,7 +142,7 @@ async function refreshCredentials(credentials: Array): Promise> ) => { const allCredentials = user.credentials; @@ -150,6 +153,7 @@ const getAllCredentials = async ( where: { teamId: eventType.team.id, }, + select: credentialForCalendarServiceSelect, }); allCredentials.push(...teamCredentialsQuery); } @@ -165,7 +169,9 @@ const getAllCredentials = async ( }, }, select: { - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, }, }); if (teamCredentialsQuery?.credentials) { @@ -180,7 +186,9 @@ const getAllCredentials = async ( id: user.organizationId, }, select: { - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, }, }); @@ -241,7 +249,9 @@ const getEventTypesFromDB = async (eventTypeId: number) => { disableGuests: true, users: { select: { - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, ...userSelect.select, }, }, @@ -317,7 +327,9 @@ const getEventTypesFromDB = async (eventTypeId: number) => { isFixed: true, user: { select: { - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, ...userSelect.select, organization: { select: { @@ -352,7 +364,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => { type IsFixedAwareUser = User & { isFixed: boolean; - credentials: Credential[]; + credentials: CredentialPayload[]; organization: { slug: string }; }; @@ -736,7 +748,9 @@ async function handler( }, select: { ...userSelect.select, - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, metadata: true, }, }); @@ -786,7 +800,9 @@ async function handler( id: eventType.userId, }, select: { - credentials: true, // Don't leak to client + credentials: { + select: credentialForCalendarServiceSelect, + }, // Don't leak to client ...userSelect.select, }, }); @@ -995,6 +1011,7 @@ async function handler( teamDestinationCalendars.push(user.destinationCalendar); } return { + id: user.id, email: user.email ?? "", name: user.name ?? "", firstName: "", @@ -1112,6 +1129,7 @@ async function handler( where: { id: reference.credentialId, }, + select: credentialForCalendarServiceSelect, }); if (credential) { diff --git a/packages/features/ee/payments/api/paypal-webhook.ts b/packages/features/ee/payments/api/paypal-webhook.ts index 06708fe0fd..06c5588795 100644 --- a/packages/features/ee/payments/api/paypal-webhook.ts +++ b/packages/features/ee/payments/api/paypal-webhook.ts @@ -16,6 +16,7 @@ import { HttpError as HttpCode } from "@calcom/lib/http-error"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { CalendarEvent } from "@calcom/types/Calendar"; export const config = { @@ -74,7 +75,9 @@ export async function handlePaymentSuccess( select: { id: true, username: true, - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, timeZone: true, email: true, name: true, diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index c6b563a071..c3e9701d12 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -6,7 +6,7 @@ import type Stripe from "stripe"; import stripe from "@calcom/app-store/stripepayment/lib/server"; import EventManager from "@calcom/core/EventManager"; import dayjs from "@calcom/dayjs"; -import { sendScheduledEmails, sendOrganizerRequestEmail, sendAttendeeRequestEmail } from "@calcom/emails"; +import { sendAttendeeRequestEmail, sendOrganizerRequestEmail, sendScheduledEmails } from "@calcom/emails"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; @@ -15,8 +15,9 @@ import { getErrorFromUnknown } from "@calcom/lib/errors"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; -import { prisma, bookingMinimalSelect } from "@calcom/prisma"; +import { bookingMinimalSelect, prisma } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -164,7 +165,7 @@ async function handlePaymentSuccess(event: Stripe.Event) { select: { id: true, username: true, - credentials: true, + credentials: { select: credentialForCalendarServiceSelect }, timeZone: true, timeFormat: true, email: true, @@ -313,7 +314,7 @@ const handleSetupSuccess = async (event: Stripe.Event) => { name: true, locale: true, destinationCalendar: true, - credentials: true, + credentials: { select: credentialForCalendarServiceSelect }, }, }); diff --git a/packages/lib/CalendarService.ts b/packages/lib/CalendarService.ts index da4a32f7fd..323ad01a64 100644 --- a/packages/lib/CalendarService.ts +++ b/packages/lib/CalendarService.ts @@ -95,7 +95,7 @@ const getDuration = (start: string, end: string): DurationObject => ({ minutes: dayjs(end).diff(dayjs(start), "minute"), }); -const getAttendees = (attendees: Person[]): Attendee[] => +const mapAttendees = (attendees: Person[]): Attendee[] => attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" })); export default abstract class BaseCalendarService implements Calendar { @@ -104,6 +104,7 @@ export default abstract class BaseCalendarService implements Calendar { private headers: Record = {}; protected integrationName = ""; private log: typeof logger; + private credential: CredentialPayload; constructor(credential: CredentialPayload, integrationName: string, url?: string) { this.integrationName = integrationName; @@ -118,11 +119,25 @@ export default abstract class BaseCalendarService implements Calendar { this.credentials = { username, password }; this.headers = getBasicAuthHeaders({ username, password }); + this.credential = credential; this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); } - async createEvent(event: CalendarEvent): Promise { + private getAttendees(event: CalendarEvent) { + const attendees = mapAttendees(event.attendees); + + if (event.team?.members) { + const teamAttendeesWithoutCurrentUser = event.team.members.filter( + (member) => member.email !== this.credential.user?.email + ); + attendees.push(...mapAttendees(teamAttendeesWithoutCurrentUser)); + } + + return attendees; + } + + async createEvent(event: CalendarEvent, credentialId: number): Promise { try { const calendars = await this.listCalendars(event); @@ -138,10 +153,7 @@ export default abstract class BaseCalendarService implements Calendar { description: getRichDescription(event), location: getLocation(event), organizer: { email: event.organizer.email, name: event.organizer.name }, - attendees: [ - ...getAttendees(event.attendees), - ...(event.team?.members ? getAttendees(event.team.members) : []), - ], + attendees: this.getAttendees(event), /** according to https://datatracker.ietf.org/doc/html/rfc2446#section-3.2.1, in a published iCalendar component. * "Attendees" MUST NOT be present * `attendees: this.getAttendees(event.attendees),` @@ -153,7 +165,10 @@ export default abstract class BaseCalendarService implements Calendar { if (error || !iCalString) throw new Error(`Error creating iCalString:=> ${error?.message} : ${error?.name} `); - const [mainHostDestinationCalendar] = event.destinationCalendar ?? []; + const mainHostDestinationCalendar = event.destinationCalendar + ? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ?? + event.destinationCalendar[0] + : undefined; // We create the event directly on iCal const responses = await Promise.all( @@ -214,10 +229,7 @@ export default abstract class BaseCalendarService implements Calendar { description: getRichDescription(event), location: getLocation(event), organizer: { email: event.organizer.email, name: event.organizer.name }, - attendees: [ - ...getAttendees(event.attendees), - ...(event.team?.members ? getAttendees(event.team.members) : []), - ], + attendees: this.getAttendees(event), }); if (error) { diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 8b0c0cbd8e..5aba14feaa 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -1,4 +1,4 @@ -import type { Prisma, Credential } from "@prisma/client"; +import type { Prisma } from "@prisma/client"; import { DailyLocationType } from "@calcom/app-store/locations"; import slugify from "@calcom/lib/slugify"; @@ -6,6 +6,7 @@ import { PeriodType, SchedulingType } from "@calcom/prisma/enums"; import type { userSelect } from "@calcom/prisma/selects"; import type { CustomInputSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import type { CredentialPayload } from "@calcom/types/Credential"; type User = Prisma.UserGetPayload; @@ -25,7 +26,7 @@ type UsernameSlugLinkProps = { slug: string; }; -const user: User & { credentials: Credential[] } = { +const user: User & { credentials: CredentialPayload[] } = { metadata: null, theme: null, credentials: [], diff --git a/packages/lib/server/getUsersCredentials.ts b/packages/lib/server/getUsersCredentials.ts index 5a4323647d..f4f1dfb493 100644 --- a/packages/lib/server/getUsersCredentials.ts +++ b/packages/lib/server/getUsersCredentials.ts @@ -1,19 +1,12 @@ import { prisma } from "@calcom/prisma"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; export async function getUsersCredentials(userId: number) { const credentials = await prisma.credential.findMany({ where: { userId, }, - select: { - id: true, - type: true, - key: true, - userId: true, - appId: true, - invalid: true, - teamId: true, - }, + select: credentialForCalendarServiceSelect, orderBy: { id: "asc", }, diff --git a/packages/prisma/selects/credential.ts b/packages/prisma/selects/credential.ts index bb002644ea..ad0730ccad 100644 --- a/packages/prisma/selects/credential.ts +++ b/packages/prisma/selects/credential.ts @@ -1,11 +1,31 @@ import { Prisma } from "@prisma/client"; +export const credentialForCalendarServiceSelect = Prisma.validator()({ + id: true, + appId: true, + type: true, + userId: true, + user: { + select: { + email: true, + }, + }, + teamId: true, + key: true, + invalid: true, +}); + export const safeCredentialSelect = Prisma.validator()({ id: true, type: true, /** Omitting to avoid frontend leaks */ // key: true, userId: true, + user: { + select: { + email: true, + }, + }, teamId: true, appId: true, invalid: true, diff --git a/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts b/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts index 41930dc07c..8c195f1546 100644 --- a/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts @@ -3,6 +3,7 @@ import type { DestinationCalendar } from "@prisma/client"; import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager"; import { prisma } from "@calcom/prisma"; import { AppCategories } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import type { TConnectedCalendarsInputSchema } from "./connectedCalendars.schema"; @@ -26,6 +27,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda enabled: true, }, }, + select: credentialForCalendarServiceSelect, }); // get user's credentials + their connected integrations diff --git a/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts b/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts index fa6de6b98b..fb716afb84 100644 --- a/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts @@ -10,9 +10,9 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { deletePayment } from "@calcom/lib/payment/deletePayment"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { bookingMinimalSelect } from "@calcom/prisma"; -import { prisma } from "@calcom/prisma"; +import { bookingMinimalSelect, prisma } from "@calcom/prisma"; import { AppCategories, BookingStatus } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -37,8 +37,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp ...(teamId ? { teamId } : { userId: ctx.user.id }), }, select: { - key: true, - appId: true, + ...credentialForCalendarServiceSelect, app: { select: { slug: true, @@ -46,11 +45,6 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp dirName: true, }, }, - id: true, - type: true, - userId: true, - teamId: true, - invalid: true, }, }); diff --git a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts index df210591db..664b04f340 100644 --- a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts @@ -1,4 +1,4 @@ -import type { Credential, Prisma } from "@prisma/client"; +import type { Prisma } from "@prisma/client"; import type { CredentialOwner } from "@calcom/app-store/types"; import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials"; @@ -6,7 +6,9 @@ import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; +import type { CredentialPayload } from "@calcom/types/Credential"; import type { TIntegrationsInputSchema } from "./integrations.schema"; @@ -20,7 +22,9 @@ type IntegrationsOptions = { type TeamQuery = Prisma.TeamGetPayload<{ select: { id: true; - credentials?: true; + credentials: { + select: typeof import("@calcom/prisma/selects/credential").credentialForCalendarServiceSelect; + }; name: true; logo: true; members: { @@ -63,7 +67,9 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = }, select: { id: true, - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, name: true, logo: true, members: { @@ -77,7 +83,9 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = parent: { select: { id: true, - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, name: true, logo: true, members: { @@ -109,7 +117,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = userTeams = [...teamsQuery, ...parentTeams]; - const teamAppCredentials: Credential[] = userTeams.flatMap((teamApp) => { + const teamAppCredentials: CredentialPayload[] = userTeams.flatMap((teamApp) => { return teamApp.credentials ? teamApp.credentials.flat() : []; }); if (!includeTeamInstalledApps || teamId) { diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index e4110f2405..cfb1017a0b 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -6,6 +6,7 @@ import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import { prisma } from "@calcom/prisma"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; import type { CredentialPayload } from "@calcom/types/Credential"; @@ -46,6 +47,7 @@ export const editLocationHandler = async ({ ctx, input }: EditLocationOptions) = where: { id: details.credentialId, }, + select: credentialForCalendarServiceSelect, }); } diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 08f1a1ceeb..61d5d5cac3 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -16,6 +16,7 @@ import getSlots from "@calcom/lib/slots"; import prisma, { availabilityUserSelect } from "@calcom/prisma"; import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { EventBusyDate } from "@calcom/types/Calendar"; @@ -177,7 +178,7 @@ export async function getEventType( isFixed: true, user: { select: { - credentials: true, + credentials: { select: credentialForCalendarServiceSelect }, ...availabilityUserSelect, }, }, @@ -185,7 +186,7 @@ export async function getEventType( }, users: { select: { - credentials: true, + credentials: { select: credentialForCalendarServiceSelect }, ...availabilityUserSelect, }, }, @@ -223,7 +224,9 @@ export async function getDynamicEventType(input: TGetScheduleInputSchema) { select: { allowDynamicBooking: true, ...availabilityUserSelect, - credentials: true, + credentials: { + select: credentialForCalendarServiceSelect, + }, }, }); const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking); diff --git a/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts b/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts index 9155758888..3545e6e321 100644 --- a/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts @@ -2,6 +2,7 @@ import { getUserAvailability } from "@calcom/core/getUserAvailability"; import { isTeamMember } from "@calcom/lib/server/queries/teams"; import { availabilityUserSelect } from "@calcom/prisma"; import { prisma } from "@calcom/prisma"; +import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; @@ -25,7 +26,9 @@ export const getMemberAvailabilityHandler = async ({ ctx, input }: GetMemberAvai include: { user: { select: { - credentials: true, // needed for getUserAvailability + credentials: { + select: credentialForCalendarServiceSelect, + }, // needed for getUserAvailability ...availabilityUserSelect, organization: { select: { diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 6cdd12f881..1be997282b 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -1,4 +1,4 @@ -import type { Prisma, DestinationCalendar, SelectedCalendar, BookingSeat } from "@prisma/client"; +import type { BookingSeat, DestinationCalendar, Prisma, SelectedCalendar } from "@prisma/client"; import type { Dayjs } from "dayjs"; import type { calendar_v3 } from "googleapis"; import type { Time } from "ical.js"; @@ -39,6 +39,7 @@ export type Person = { }; export type TeamMember = { + id?: number; name: string; email: string; timeZone: string; diff --git a/packages/types/Credential.d.ts b/packages/types/Credential.d.ts index 359b09b1b0..b794bbee3f 100644 --- a/packages/types/Credential.d.ts +++ b/packages/types/Credential.d.ts @@ -6,20 +6,10 @@ import type { Prisma } from "@prisma/client"; * Also there may be a better place to save this. */ export type CredentialPayload = Prisma.CredentialGetPayload<{ - select: { - id: true; - appId: true; - type: true; - userId: true; - teamId: true; - key: true; - invalid: true; - }; + select: typeof import("@calcom/prisma/selects/credential").credentialForCalendarServiceSelect; }>; export type CredentialFrontendPayload = Omit & { /** We should type error if keys are leaked to the frontend */ key?: never; }; - -export type CredentialWithAppName = CredentialPayload & { appName: string };