* Add endpoints for testing the flow * Add MVP * new route * Fixes * Fixes * Remove enable toggle support from domainWideDelegation * Fixes * Revert "Remove enable toggle support from domainWideDelegation" This reverts commit c29e729206cd1fa063f9c9ce0cf148ef1577d60b. * Revert yarn.lock * More fixes * Fix new workspace platform add * refactor: improvements * refactor: bug fixes and improvements * fix: type errors * fix: conflicts * chore: update test * fix: logic * chore: improvements * fix: toglle * fix: bugs * fix: type err * chore: check number type * fix: after conflicts * chore: fix type err * fix: type errors and tests * fix: tets * test * chore: remove unused * fix: google meet url on booking page and secondary calendar * fix: add property * fix: type err * fix: re assingment bug * fix: use getAllCredentials * chore: fix import * fix: installed count * fix: pass event type * fix: import * fix: [Stacked PR] Review fixes (#17958) * Review fixes * fix: destination calendar bug --------- Co-authored-by: Udit Takkar <udit222001@gmail.com> * refactor: use repository * chore: remove duplicate * fix: More review fixes for domain wide delegation (#17969) * Reuse buildCredentialPayloadForCalendar * fixes --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * fix ts error * Fix getSchedule not using dwd credentials (#17995) * fix: Remove direct DWD table access from google-calendar and remove delegatedToId column from Credential (#18015) * Remove fn rename to reduce number of files changed * chore: check feature globally * test: add unit tests * chore: move function * test: add booking test * wip * Remove domain-wide-delegation team feature flag * Make sure duplicate calendars are not shown due to DWD. Show DWD when there is conflict * Fix tests and their ts errors * Fix more tests * Move findUsersForAvailabilityCheck to separate file as it has AppStore dependency causing problem with Routing Forms. Build crashes * fix: Multiple calendar connections from Google not showing up in apps/installed * DestinationCalendar must have either credentialId or domainWideDeelgationId. Also handle the case when DWD is disabled and there was a non-dwd credential that could be used * Disable deletetion of DWD as it is destructive and prefer disabling instead * Show DWD credential calendars at the top * Fixed tests * Calendar Cache DWD support * Self-Review: Verify email required and add more selectedCalendar tests * Self Review: shorten names * Self ReviewL Dead code removal * Revert "Calendar Cache DWD support" This reverts commit 009f236470fa21eba5986117d4f6e4d4c5e38c34. * Some misc fixes * fixes * Performance improvememt in slots loading and booking * More cases for handling dwd credentials * simplify the logic. Ensure that EventManager and the modules below it in the flow have CredentialForCalendarService available * Fix tests * Fix installed/conferencing not showing gogole meet * Shorten name * Fix ApI v2 tests * Add some more tests * add getSchedule tests * Improve tests * Make Google Meet default when DWD for google is enabled * Enable feature flagging for DWD * chore: bump libraries * Encrypt serviceaccount private key * Fix ts errors * bump platform libraries * org add dwd * org add dwd * bump platform libraries * fix selected calendars * fix remove selected dwd calendar * remove oauthclient id aliad in authedCalendar * remove oauthclient id aliad in authedCalendar * refactor: OrganizationsDwdController * chore: export toggleDwdEnabled from platform-libraries * feat: v2 update (enable / disable) dwd * refactor: SelectedCalendarsController check if user belongs to dwd org & for not dwd use previous logic * wip: DestinationCalendar send domainWideDelegationCredentialId from frontend to api * try fix set destination calendar api v2 * fixup! try fix set destination calendar api v2 * setting google meet as default location working for dwd * bump platform libraries version * allow office 365 workspace slug * allow office 365 workspace slug * chore: v2 create dwd MS and Google service keys input * fix: CreateDwdInput serviceAccountKey * fix: CredentialForCalendarService type * fix: check workspace slug * feat: update serviceAccessKey of DWD * testing * fixup! testing * fix: getAuthUtl bug * fix: unit tests * fix: type err and unit test * fix: e2e test * fix: credentials bug and failing unit tests * Update CalendarService.ts * chore: refactor office365 calendar service and add testDomainWideDelegationSetup * fix: office365Calendar use correct clientId/Secret for DWD * fix: office365Calendar dwd no need refresh token * test: add unit test for outlook dwd and setup * feat: added dwd support for office365 calendar * feat: added dwd support for office365 video * test: finish test for outlook dwd * fix: create dialog bug * chore: remove console logs * fix: refreshToken bug * bump version libraries * refactor: fetch dwd credentials only in findQualifiedHosts * fixup! refactor: fetch dwd credentials only in findQualifiedHosts * fix: type err * fix: type err * fix: getUserDisplayName * fix: unit test * chore: bump platform lib --------- Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
// eslint-disable-next-line no-restricted-imports
|
|
import { sortBy } from "lodash";
|
|
|
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
|
import getApps from "@calcom/app-store/utils";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { getUid } from "@calcom/lib/CalEventParser";
|
|
import { CalendarAppDomainWideDelegationError } from "@calcom/lib/CalendarAppError";
|
|
import { buildNonDwdCredentials } from "@calcom/lib/domainWideDelegation/clientAndServer";
|
|
import logger from "@calcom/lib/logger";
|
|
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import type {
|
|
CalendarEvent,
|
|
EventBusyDate,
|
|
IntegrationCalendar,
|
|
NewCalendarEventType,
|
|
SelectedCalendar,
|
|
} from "@calcom/types/Calendar";
|
|
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
|
|
import type { EventResult } from "@calcom/types/EventManager";
|
|
|
|
import getCalendarsEvents from "./getCalendarsEvents";
|
|
import { getCalendarsEventsWithTimezones } from "./getCalendarsEvents";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["CalendarManager"] });
|
|
|
|
export const getCalendarCredentials = (credentials: Array<CredentialForCalendarService>) => {
|
|
const calendarCredentials = getApps(credentials, true)
|
|
.filter((app) => app.type.endsWith("_calendar"))
|
|
.flatMap((app) => {
|
|
const credentials = app.credentials.flatMap((credential) => {
|
|
const calendar = getCalendar(credential);
|
|
return app.variant === "calendar" ? [{ integration: app, credential, calendar }] : [];
|
|
});
|
|
|
|
return credentials.length ? credentials : [];
|
|
});
|
|
|
|
return calendarCredentials;
|
|
};
|
|
|
|
export const getCalendarCredentialsWithoutDwd = (credentials: CredentialPayload[]) => {
|
|
return getCalendarCredentials(buildNonDwdCredentials(credentials));
|
|
};
|
|
|
|
export const getConnectedCalendars = async (
|
|
calendarCredentials: ReturnType<typeof getCalendarCredentials>,
|
|
selectedCalendars: { externalId: string }[],
|
|
destinationCalendarExternalId?: string
|
|
) => {
|
|
let destinationCalendar: IntegrationCalendar | undefined;
|
|
const connectedCalendars = await Promise.all(
|
|
calendarCredentials.map(async (item) => {
|
|
try {
|
|
const { integration, credential } = item;
|
|
const calendar = await item.calendar;
|
|
// Don't leak credentials to the client
|
|
const credentialId = credential.id;
|
|
const domainWideDelegationCredentialId = credential.delegatedToId ?? null;
|
|
if (!calendar) {
|
|
return {
|
|
integration,
|
|
credentialId,
|
|
domainWideDelegationCredentialId,
|
|
};
|
|
}
|
|
const cals = await calendar.listCalendars();
|
|
const calendars = sortBy(
|
|
cals.map((cal: IntegrationCalendar) => {
|
|
if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal;
|
|
return {
|
|
...cal,
|
|
readOnly: cal.readOnly || false,
|
|
primary: cal.primary || null,
|
|
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
|
credentialId,
|
|
domainWideDelegationCredentialId,
|
|
};
|
|
}),
|
|
["primary"]
|
|
);
|
|
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
|
if (!primary) {
|
|
return {
|
|
integration,
|
|
credentialId,
|
|
error: {
|
|
message: "No primary calendar found",
|
|
},
|
|
};
|
|
}
|
|
// HACK https://github.com/calcom/cal.com/pull/7644/files#r1131508414
|
|
if (destinationCalendar && !Object.isFrozen(destinationCalendar)) {
|
|
destinationCalendar.primaryEmail = primary.email;
|
|
destinationCalendar.integrationTitle = integration.title;
|
|
destinationCalendar = Object.freeze(destinationCalendar);
|
|
}
|
|
|
|
return {
|
|
integration: cleanIntegrationKeys(integration),
|
|
credentialId,
|
|
domainWideDelegationCredentialId,
|
|
primary,
|
|
calendars,
|
|
};
|
|
} 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";
|
|
}
|
|
}
|
|
|
|
if (error instanceof CalendarAppDomainWideDelegationError) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
log.error("getConnectedCalendars failed", safeStringify(error), safeStringify({ item }));
|
|
|
|
return {
|
|
integration: cleanIntegrationKeys(item.integration),
|
|
credentialId: item.credential.id,
|
|
domainWideDelegationCredentialId: item.credential.delegatedToId,
|
|
error: {
|
|
message: errorMessage,
|
|
},
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
return { connectedCalendars, destinationCalendar };
|
|
};
|
|
|
|
/**
|
|
* Important function to prevent leaking credentials to the client
|
|
* @param appIntegration
|
|
* @returns App
|
|
*/
|
|
const cleanIntegrationKeys = (
|
|
appIntegration: Awaited<ReturnType<typeof getCalendarCredentials>>[number]["integration"] & {
|
|
credentials?: Array<CredentialPayload>;
|
|
credential: CredentialPayload;
|
|
}
|
|
) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { credentials, credential, ...rest } = appIntegration;
|
|
return rest;
|
|
};
|
|
|
|
export const getBusyCalendarTimes = async (
|
|
/**
|
|
* withCredentials can possibly have a duplicate credential in case DWD is enabled.
|
|
* There is no way to deduplicate that at the moment because a `credential` doesn't directly know for which email it is,
|
|
*/
|
|
withCredentials: CredentialForCalendarService[],
|
|
dateFrom: string,
|
|
dateTo: string,
|
|
selectedCalendars: SelectedCalendar[],
|
|
shouldServeCache?: boolean,
|
|
includeTimeZone?: boolean
|
|
) => {
|
|
let results: (EventBusyDate & { timeZone?: string })[][] = [];
|
|
// const months = getMonths(dateFrom, dateTo);
|
|
try {
|
|
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
|
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
|
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
|
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
|
|
|
|
if (includeTimeZone) {
|
|
results = await getCalendarsEventsWithTimezones(withCredentials, startDate, endDate, selectedCalendars);
|
|
} else {
|
|
results = await getCalendarsEvents(
|
|
withCredentials,
|
|
startDate,
|
|
endDate,
|
|
selectedCalendars,
|
|
shouldServeCache
|
|
);
|
|
}
|
|
} catch (e) {
|
|
log.warn(safeStringify(e));
|
|
}
|
|
return results.reduce((acc, availability) => acc.concat(availability), []);
|
|
};
|
|
|
|
export const createEvent = async (
|
|
credential: CredentialForCalendarService,
|
|
calEvent: CalendarEvent,
|
|
externalId?: string
|
|
): Promise<EventResult<NewCalendarEventType>> => {
|
|
const uid: string = getUid(calEvent);
|
|
const calendar = await getCalendar(credential);
|
|
let success = true;
|
|
let calError: string | undefined = undefined;
|
|
|
|
log.debug(
|
|
"Creating calendar event",
|
|
safeStringify({
|
|
calEvent: getPiiFreeCalendarEvent(calEvent),
|
|
})
|
|
);
|
|
// Check if the disabledNotes flag is set to true
|
|
if (calEvent.hideCalendarNotes) {
|
|
calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
|
|
}
|
|
|
|
const externalCalendarIdWhenDwdCredentialIsChosen = credential.delegatedToId ? externalId : undefined;
|
|
|
|
// TODO: Surface success/error messages coming from apps to improve end user visibility
|
|
const creationResult = calendar
|
|
? await calendar
|
|
// Ideally we should pass externalId always, but let's start with DWD case first as in that case, CalendarService need to handle a special case for DWD to determine the selectedCalendar.
|
|
// Such logic shouldn't exist in CalendarService as it would be same for all calendar apps.
|
|
.createEvent(calEvent, credential.id, externalCalendarIdWhenDwdCredentialIsChosen)
|
|
.catch(async (error: { code: number; calError: string }) => {
|
|
success = false;
|
|
/**
|
|
* There is a time when selectedCalendar externalId doesn't match witch certain credential
|
|
* so google returns 404.
|
|
* */
|
|
if (error?.code === 404) {
|
|
return undefined;
|
|
}
|
|
if (error?.calError) {
|
|
calError = error.calError;
|
|
}
|
|
log.error(
|
|
"createEvent failed",
|
|
safeStringify(error),
|
|
safeStringify({ calEvent: getPiiFreeCalendarEvent(calEvent) })
|
|
);
|
|
// @TODO: This code will be off till we can investigate an error with it
|
|
//https://github.com/calcom/cal.com/issues/3949
|
|
// await sendBrokenIntegrationEmail(calEvent, "calendar");
|
|
return undefined;
|
|
})
|
|
: undefined;
|
|
if (!creationResult) {
|
|
logger.error(
|
|
"createEvent failed",
|
|
safeStringify({
|
|
success,
|
|
uid,
|
|
creationResult,
|
|
originalEvent: getPiiFreeCalendarEvent(calEvent),
|
|
calError,
|
|
})
|
|
);
|
|
}
|
|
log.debug(
|
|
"Created calendar event",
|
|
safeStringify({
|
|
calEvent: getPiiFreeCalendarEvent(calEvent),
|
|
creationResult,
|
|
})
|
|
);
|
|
return {
|
|
appName: credential.appId || "",
|
|
type: credential.type,
|
|
success,
|
|
uid,
|
|
iCalUID: creationResult?.iCalUID || undefined,
|
|
createdEvent: creationResult,
|
|
originalEvent: calEvent,
|
|
calError,
|
|
calWarnings: creationResult?.additionalInfo?.calWarnings || [],
|
|
externalId,
|
|
credentialId: credential.id,
|
|
delegatedToId: credential.delegatedToId ?? undefined,
|
|
};
|
|
};
|
|
|
|
export const updateEvent = async (
|
|
credential: CredentialForCalendarService,
|
|
calEvent: CalendarEvent,
|
|
bookingRefUid: string | null,
|
|
externalCalendarId: string | null
|
|
): Promise<EventResult<NewCalendarEventType>> => {
|
|
const uid = getUid(calEvent);
|
|
const calendar = await getCalendar(credential);
|
|
let success = false;
|
|
let calError: string | undefined = undefined;
|
|
let calWarnings: string[] | undefined = [];
|
|
log.debug(
|
|
"Updating calendar event",
|
|
safeStringify({
|
|
bookingRefUid,
|
|
calEvent: getPiiFreeCalendarEvent(calEvent),
|
|
})
|
|
);
|
|
if (bookingRefUid === "") {
|
|
log.error(
|
|
"updateEvent failed",
|
|
"bookingRefUid is empty",
|
|
safeStringify({ calEvent: getPiiFreeCalendarEvent(calEvent) })
|
|
);
|
|
}
|
|
const updatedResult: NewCalendarEventType | NewCalendarEventType[] | undefined =
|
|
calendar && bookingRefUid
|
|
? await calendar
|
|
.updateEvent(bookingRefUid, calEvent, externalCalendarId)
|
|
.then((event: NewCalendarEventType | NewCalendarEventType[]) => {
|
|
success = true;
|
|
return event;
|
|
})
|
|
.catch(async (e: { calError: string }) => {
|
|
// @TODO: This code will be off till we can investigate an error with it
|
|
// @see https://github.com/calcom/cal.com/issues/3949
|
|
// await sendBrokenIntegrationEmail(calEvent, "calendar");
|
|
log.error(
|
|
"updateEvent failed",
|
|
safeStringify({ e, calEvent: getPiiFreeCalendarEvent(calEvent) })
|
|
);
|
|
if (e?.calError) {
|
|
calError = e.calError;
|
|
}
|
|
return undefined;
|
|
})
|
|
: undefined;
|
|
|
|
if (!updatedResult) {
|
|
logger.error(
|
|
"updateEvent failed",
|
|
safeStringify({
|
|
success,
|
|
bookingRefUid,
|
|
credential: getPiiFreeCredential(credential),
|
|
originalEvent: getPiiFreeCalendarEvent(calEvent),
|
|
calError,
|
|
})
|
|
);
|
|
}
|
|
|
|
if (Array.isArray(updatedResult)) {
|
|
calWarnings = updatedResult.flatMap((res) => res.additionalInfo?.calWarnings ?? []);
|
|
} else {
|
|
calWarnings = updatedResult?.additionalInfo?.calWarnings || [];
|
|
}
|
|
|
|
return {
|
|
appName: credential.appId || "",
|
|
type: credential.type,
|
|
success,
|
|
uid,
|
|
updatedEvent: updatedResult,
|
|
originalEvent: calEvent,
|
|
calError,
|
|
calWarnings,
|
|
};
|
|
};
|
|
|
|
export const deleteEvent = async ({
|
|
credential,
|
|
bookingRefUid,
|
|
event,
|
|
externalCalendarId,
|
|
}: {
|
|
credential: CredentialForCalendarService;
|
|
bookingRefUid: string;
|
|
event: CalendarEvent;
|
|
externalCalendarId?: string | null;
|
|
}): Promise<unknown> => {
|
|
const calendar = await getCalendar(credential);
|
|
log.debug(
|
|
"Deleting calendar event",
|
|
safeStringify({
|
|
bookingRefUid,
|
|
event: getPiiFreeCalendarEvent(event),
|
|
})
|
|
);
|
|
if (calendar) {
|
|
return calendar.deleteEvent(bookingRefUid, event, externalCalendarId);
|
|
} else {
|
|
log.error(
|
|
"Could not do deleteEvent - No calendar adapter found",
|
|
safeStringify({
|
|
credential: getPiiFreeCredential(credential),
|
|
event,
|
|
})
|
|
);
|
|
}
|
|
|
|
return Promise.resolve({});
|
|
};
|