Skip to content

Commit 2f16758

Browse files
fix: improve error handling in delegation credential workspace configuration (calcom#26985)
- Changed checkIfSuccessfullyConfiguredInWorkspace to throw detailed errors instead of returning boolean - Updated testDelegationCredentialSetup in Google Calendar and Office365 Calendar services to throw specific errors - Updated assertWorkspaceConfigured to let errors propagate naturally - Updated Calendar interface type to reflect Promise<void> return type - Updated CalendarCacheWrapper and CalendarTelemetryWrapper to match new interface Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 04e8283 commit 2f16758

7 files changed

Lines changed: 123 additions & 113 deletions

File tree

packages/app-store/delegationCredential.ts

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -301,20 +301,17 @@ async function _getDelegationCredentialsMapPerUser({
301301
return credentialsByUserId;
302302
}
303303

304-
export async function checkIfSuccessfullyConfiguredInWorkspace({
304+
export async function assertSuccessfullyConfiguredInWorkspace({
305305
delegationCredential,
306306
user,
307307
}: {
308308
delegationCredential: DelegationCredentialWithSensitiveServiceAccountKey;
309309
user: User;
310-
}) {
310+
}): Promise<void> {
311311
if (!isValidWorkspaceSlug(delegationCredential.workspacePlatform.slug)) {
312-
log.warn(
313-
`Only ${WORKSPACE_PLATFORM_SLUGS.toString()} Platforms are supported here, skipping ${
314-
delegationCredential.workspacePlatform.slug
315-
}`
316-
);
317-
return false;
312+
const errorMessage = `Unsupported workspace platform: ${delegationCredential.workspacePlatform.slug}. Only ${WORKSPACE_PLATFORM_SLUGS.toString()} are supported.`;
313+
log.warn(errorMessage);
314+
throw new Error(errorMessage);
318315
}
319316

320317
const credential = _buildDelegatedCalendarCredentialWithServiceAccountKey({
@@ -325,9 +322,10 @@ export async function checkIfSuccessfullyConfiguredInWorkspace({
325322
const calendar = await getCalendar(credential, "none");
326323

327324
if (!calendar) {
328-
throw new Error("Google Calendar App not found");
325+
throw new Error("Calendar App not found for the workspace platform");
329326
}
330-
return await calendar?.testDelegationCredentialSetup?.();
327+
328+
await calendar.testDelegationCredentialSetup?.();
331329
}
332330

333331
export async function getAllDelegationCredentialsForUserByAppType({
@@ -383,26 +381,29 @@ export const buildAllCredentials = ({
383381
...buildNonDelegationCredentials(nonDelegationCredentials),
384382
];
385383

386-
const uniqueAllCredentials = allCredentials.reduce((acc, credential) => {
387-
if (!credential.delegatedToId) {
388-
// Regular credential go as is
389-
acc.push(credential);
384+
const uniqueAllCredentials = allCredentials.reduce(
385+
(acc, credential) => {
386+
if (!credential.delegatedToId) {
387+
// Regular credential go as is
388+
acc.push(credential);
389+
return acc;
390+
}
391+
const existingDelegationCredential = acc.find(
392+
(c) => c.delegatedToId === credential.delegatedToId && c.appId === credential.appId
393+
);
394+
if (!existingDelegationCredential) {
395+
acc.push(credential);
396+
}
390397
return acc;
391-
}
392-
const existingDelegationCredential = acc.find(
393-
(c) => c.delegatedToId === credential.delegatedToId && c.appId === credential.appId
394-
);
395-
if (!existingDelegationCredential) {
396-
acc.push(credential);
397-
}
398-
return acc;
399-
}, [] as typeof allCredentials);
398+
},
399+
[] as typeof allCredentials
400+
);
400401

401402
return uniqueAllCredentials;
402403
};
403404

404405
export async function enrichUsersWithDelegationCredentials<
405-
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
406+
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
406407
>({ orgId, users }: { orgId: number | null; users: TUser[] }) {
407408
const delegationCredentialsMap = await _getDelegationCredentialsMapPerUser({
408409
organizationId: orgId,
@@ -426,7 +427,7 @@ export async function enrichUsersWithDelegationCredentials<
426427

427428
export const enrichHostsWithDelegationCredentials = async <
428429
THost extends Host<TUser>,
429-
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
430+
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
430431
>({
431432
orgId,
432433
hosts,
@@ -467,7 +468,7 @@ export const enrichHostsWithDelegationCredentials = async <
467468
};
468469

469470
export const enrichUserWithDelegationCredentialsIncludeServiceAccountKey = async <
470-
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
471+
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
471472
>({
472473
user,
473474
}: {
@@ -487,7 +488,7 @@ export const enrichUserWithDelegationCredentialsIncludeServiceAccountKey = async
487488
};
488489

489490
export const enrichUserWithDelegationCredentials = async <
490-
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
491+
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
491492
>({
492493
user,
493494
}: {
@@ -503,7 +504,7 @@ export const enrichUserWithDelegationCredentials = async <
503504
};
504505

505506
export async function enrichUserWithDelegationConferencingCredentialsWithoutOrgId<
506-
TUser extends { id: number; email: string; credentials: CredentialPayload[] }
507+
TUser extends { id: number; email: string; credentials: CredentialPayload[] },
507508
>({ user }: { user: TUser }) {
508509
const { credentials, ...restUser } = await enrichUserWithDelegationCredentialsIncludeServiceAccountKey({
509510
user,
@@ -518,7 +519,7 @@ export async function enrichUserWithDelegationConferencingCredentialsWithoutOrgI
518519
* Either get Delegation credential from delegationCredentials or find regular credential from Credential table
519520
*/
520521
export async function getDelegationCredentialOrFindRegularCredential<
521-
TDelegationCredential extends { delegatedToId?: string | null }
522+
TDelegationCredential extends { delegatedToId?: string | null },
522523
>({
523524
id,
524525
delegationCredentials,
@@ -532,17 +533,17 @@ export async function getDelegationCredentialOrFindRegularCredential<
532533
return id.delegationCredentialId
533534
? delegationCredentials.find((cred) => cred.delegatedToId === id.delegationCredentialId)
534535
: id.credentialId
535-
? await CredentialRepository.findCredentialForCalendarServiceById({
536-
id: id.credentialId,
537-
})
538-
: null;
536+
? await CredentialRepository.findCredentialForCalendarServiceById({
537+
id: id.credentialId,
538+
})
539+
: null;
539540
}
540541

541542
/**
542543
* Utility function to find a credential from a list of credentials, supporting both regular and DelegationCredential credentials
543544
*/
544545
export function getDelegationCredentialOrRegularCredential<
545-
TCredential extends { delegatedToId?: string | null; id: number }
546+
TCredential extends { delegatedToId?: string | null; id: number },
546547
>({
547548
credentials,
548549
id,

packages/app-store/googlecalendar/lib/CalendarService.ts

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import type { calendar_v3 } from "@googleapis/calendar";
3-
import type { GaxiosResponse } from "googleapis-common";
4-
import { RRule } from "rrule";
2+
53
import { MeetLocationType } from "@calcom/app-store/constants";
4+
import { getDestinationCalendarRepository } from "@calcom/features/di/containers/DestinationCalendar";
65
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
76
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
87
import logger from "@calcom/lib/logger";
98
import { safeStringify } from "@calcom/lib/safeStringify";
10-
import { getDestinationCalendarRepository } from "@calcom/features/di/containers/DestinationCalendar";
119
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
1210
import type { Prisma } from "@calcom/prisma/client";
1311
import type {
1412
Calendar,
15-
CalendarServiceEvent,
1613
CalendarEvent,
14+
CalendarServiceEvent,
1715
EventBusyDate,
1816
GetAvailabilityParams,
1917
IntegrationCalendar,
2018
NewCalendarEventType,
2119
SelectedCalendarEventTypeIds,
2220
} from "@calcom/types/Calendar";
2321
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
22+
import type { calendar_v3 } from "@googleapis/calendar";
23+
import type { GaxiosResponse } from "googleapis-common";
24+
import { RRule } from "rrule";
2425

2526
import { AxiosLikeResponseToFetchResponse } from "../../_utils/oauth/AxiosLikeResponseToFetchResponse";
2627
import { CalendarAuth } from "./CalendarAuth";
@@ -68,7 +69,8 @@ interface GoogleCalError extends Error {
6869
code?: number;
6970
}
7071

71-
const isGaxiosResponse= (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
72+
const isGaxiosResponse = (error: unknown): error is GaxiosResponse<calendar_v3.Schema$Event> =>
73+
// biome-ignore lint/suspicious/noPrototypeBuiltins: Object.hasOwn not available in all build targets
7274
typeof error === "object" && !!error && Object.prototype.hasOwnProperty.call(error, "config");
7375

7476
class GoogleCalendarService implements Calendar {
@@ -497,9 +499,7 @@ class GoogleCalendarService implements Calendar {
497499
return apiResponse.json;
498500
}
499501

500-
async getFreeBusyResult(
501-
args: FreeBusyArgs,
502-
): Promise<calendar_v3.Schema$FreeBusyResponse> {
502+
async getFreeBusyResult(args: FreeBusyArgs): Promise<calendar_v3.Schema$FreeBusyResponse> {
503503
return await this.fetchAvailability(args);
504504
}
505505

@@ -516,22 +516,23 @@ class GoogleCalendarService implements Calendar {
516516
return validCals[0];
517517
}
518518

519-
async getFreeBusyData(
520-
args: FreeBusyArgs,
521-
): Promise<(EventBusyDate & { id: string })[] | null> {
519+
async getFreeBusyData(args: FreeBusyArgs): Promise<(EventBusyDate & { id: string })[] | null> {
522520
const freeBusyResult = await this.getFreeBusyResult(args);
523521
if (!freeBusyResult.calendars) return null;
524522

525-
const result = Object.entries(freeBusyResult.calendars).reduce((c, [id, i]) => {
526-
i.busy?.forEach((busyTime) => {
527-
c.push({
528-
id,
529-
start: busyTime.start || "",
530-
end: busyTime.end || "",
523+
const result = Object.entries(freeBusyResult.calendars).reduce(
524+
(c, [id, i]) => {
525+
i.busy?.forEach((busyTime) => {
526+
c.push({
527+
id,
528+
start: busyTime.start || "",
529+
end: busyTime.end || "",
530+
});
531531
});
532-
});
533-
return c;
534-
}, [] as (EventBusyDate & { id: string })[]);
532+
return c;
533+
},
534+
[] as (EventBusyDate & { id: string })[]
535+
);
535536

536537
return result;
537538
}
@@ -646,7 +647,7 @@ class GoogleCalendarService implements Calendar {
646647
private async fetchAvailabilityData(
647648
calendarIds: string[],
648649
dateFrom: string,
649-
dateTo: string,
650+
dateTo: string
650651
): Promise<EventBusyDate[]> {
651652
// More efficient date difference calculation using native Date objects
652653
// Use Math.floor to match dayjs diff behavior (truncates, doesn't round up)
@@ -657,13 +658,11 @@ class GoogleCalendarService implements Calendar {
657658

658659
// Google API only allows a date range of 90 days for /freebusy
659660
if (diff <= 90) {
660-
const freeBusyData = await this.getFreeBusyData(
661-
{
662-
timeMin: dateFrom,
663-
timeMax: dateTo,
664-
items: calendarIds.map((id) => ({ id })),
665-
}
666-
);
661+
const freeBusyData = await this.getFreeBusyData({
662+
timeMin: dateFrom,
663+
timeMax: dateTo,
664+
items: calendarIds.map((id) => ({ id })),
665+
});
667666

668667
if (!freeBusyData) throw new Error("No response from google calendar");
669668
return freeBusyData.map((freeBusy) => ({ start: freeBusy.start, end: freeBusy.end }));
@@ -685,13 +684,11 @@ class GoogleCalendarService implements Calendar {
685684
currentEndTime = originalEndTime;
686685
}
687686

688-
const chunkData = await this.getFreeBusyData(
689-
{
690-
timeMin: new Date(currentStartTime).toISOString(),
691-
timeMax: new Date(currentEndTime).toISOString(),
692-
items: calendarIds.map((id) => ({ id })),
693-
}
694-
);
687+
const chunkData = await this.getFreeBusyData({
688+
timeMin: new Date(currentStartTime).toISOString(),
689+
timeMax: new Date(currentEndTime).toISOString(),
690+
items: calendarIds.map((id) => ({ id })),
691+
});
695692

696693
if (chunkData) {
697694
busyData.push(...chunkData.map((freeBusy) => ({ start: freeBusy.start, end: freeBusy.end })));
@@ -757,20 +754,36 @@ class GoogleCalendarService implements Calendar {
757754
primary: cal.primary ?? false,
758755
readOnly: !(cal.accessRole === "writer" || cal.accessRole === "owner") && true,
759756
email: cal.id ?? "",
760-
} satisfies IntegrationCalendar)
757+
}) satisfies IntegrationCalendar
761758
);
762759
} catch (error) {
763760
this.log.error("There was an error getting calendars: ", safeStringify(error));
764761
throw error;
765762
}
766763
}
767764

768-
// It would error if the delegation credential is not set up correctly
769-
async testDelegationCredentialSetup() {
765+
async testDelegationCredentialSetup(): Promise<void> {
770766
log.debug("Testing delegation credential setup");
771-
const calendar = await this.authedCalendar();
772-
const cals = await calendar.calendarList.list({ fields: "items(id)" });
773-
return !!cals.data.items;
767+
try {
768+
const calendar = await this.authedCalendar();
769+
const cals = await calendar.calendarList.list({ fields: "items(id)" });
770+
if (!cals.data.items) {
771+
throw new Error("No calendars found - delegation credential may not have proper access");
772+
}
773+
} catch (error) {
774+
const googleError = error as { code?: number; message?: string };
775+
if (googleError.code === 401) {
776+
throw new Error(
777+
`Google API authentication failed: ${googleError.message || "Invalid credentials or insufficient permissions"}`
778+
);
779+
}
780+
if (googleError.code === 403) {
781+
throw new Error(
782+
`Google API access denied: ${googleError.message || "The service account may not have domain-wide delegation enabled or the required scopes"}`
783+
);
784+
}
785+
throw error;
786+
}
774787
}
775788

776789
async createSelectedCalendar(
@@ -885,9 +898,7 @@ class GoogleCalendarService implements Calendar {
885898
* from leaking into the emitted .d.ts file, which would cause TypeScript to load
886899
* all Google API SDK declaration files when type-checking dependent packages.
887900
*/
888-
export default function BuildCalendarService(
889-
credential: CredentialForCalendarServiceWithEmail
890-
): Calendar {
901+
export default function BuildCalendarService(credential: CredentialForCalendarServiceWithEmail): Calendar {
891902
return new GoogleCalendarService(credential);
892903
}
893904

0 commit comments

Comments
 (0)