diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index b969d777b2..e943c9fcaf 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -1,10 +1,8 @@ import { Credential, Prisma } from "@prisma/client"; -import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client"; -import { Auth, calendar_v3, google } from "googleapis"; +import { calendar_v3, google } from "googleapis"; import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; import CalendarService from "@calcom/lib/CalendarService"; -import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { @@ -15,81 +13,63 @@ import type { NewCalendarEventType, } from "@calcom/types/Calendar"; -import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import { getGoogleAppKeys } from "./getGoogleAppKeys"; +import { googleCredentialSchema } from "./googleCredentialSchema"; interface GoogleCalError extends Error { code?: number; } export default class GoogleCalendarService implements Calendar { - private url = ""; private integrationName = ""; - private auth: Promise<{ getToken: () => Promise }>; + private auth: { getToken: () => Promise }; private log: typeof logger; - private client_id = ""; - private client_secret = ""; - private redirect_uri = ""; constructor(credential: Credential) { this.integrationName = "google_calendar"; - - this.auth = this.googleAuth(credential).then((m) => m); - + this.auth = this.googleAuth(credential); this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] }); } - private googleAuth = async (credential: Credential) => { - const appKeys = await getAppKeysFromSlug("google-calendar"); - if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id; - if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret; - if (typeof appKeys.redirect_uris === "object" && Array.isArray(appKeys.redirect_uris)) { - this.redirect_uri = appKeys.redirect_uris[0] as string; + private googleAuth = (credential: Credential) => { + const googleCredentials = googleCredentialSchema.parse(credential.key); + + async function getGoogleAuth() { + const { client_id, client_secret, redirect_uris } = await getGoogleAppKeys(); + const myGoogleAuth = new MyGoogleAuth(client_id, client_secret, redirect_uris[0]); + myGoogleAuth.setCredentials(googleCredentials); + return myGoogleAuth; } - if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." }); - if (!this.client_secret) - throw new HttpError({ statusCode: 400, message: "Google client_secret missing." }); - if (!this.redirect_uri) throw new HttpError({ statusCode: 400, message: "Google redirect_uri missing." }); - const myGoogleAuth = new MyGoogleAuth(this.client_id, this.client_secret, this.redirect_uri); - - const googleCredentials = credential.key as Auth.Credentials; - myGoogleAuth.setCredentials(googleCredentials); - - const isExpired = () => myGoogleAuth.isTokenExpiring(); - - const refreshAccessToken = () => - myGoogleAuth - .refreshToken(googleCredentials.refresh_token) - .then(async (res: GetTokenResponse) => { - const token = res.res?.data; - googleCredentials.access_token = token.access_token; - googleCredentials.expiry_date = token.expiry_date; - await prisma.credential.update({ - where: { - id: credential.id, - }, - data: { - key: googleCredentials as Prisma.InputJsonValue, - }, - }); - myGoogleAuth.setCredentials(googleCredentials); - return myGoogleAuth; - }) - .catch((err) => { - this.log.error("Error refreshing google token", err); - - return myGoogleAuth; + const refreshAccessToken = async (myGoogleAuth: Awaited>) => { + try { + const { res } = await myGoogleAuth.refreshToken(googleCredentials.refresh_token); + const token = res?.data; + googleCredentials.access_token = token.access_token; + googleCredentials.expiry_date = token.expiry_date; + const key = googleCredentialSchema.parse(googleCredentials); + await prisma.credential.update({ + where: { id: credential.id }, + data: { key }, }); - + myGoogleAuth.setCredentials(googleCredentials); + } catch (err) { + this.log.error("Error refreshing google token", err); + } + return myGoogleAuth; + }; return { - getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()), + getToken: async () => { + const myGoogleAuth = await getGoogleAuth(); + const isExpired = () => myGoogleAuth.isTokenExpiring(); + return !isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken(myGoogleAuth); + }, }; }; async createEvent(calEventRaw: CalendarEvent): Promise { return new Promise(async (resolve, reject) => { - const auth = await this.auth; - const myGoogleAuth = await auth.getToken(); + const myGoogleAuth = await this.auth.getToken(); const payload: calendar_v3.Schema$Event = { summary: calEventRaw.title, description: getRichDescription(calEventRaw), @@ -168,8 +148,7 @@ export default class GoogleCalendarService implements Calendar { async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise { return new Promise(async (resolve, reject) => { - const auth = await this.auth; - const myGoogleAuth = await auth.getToken(); + const myGoogleAuth = await this.auth.getToken(); const payload: calendar_v3.Schema$Event = { summary: event.title, description: getRichDescription(event), @@ -254,8 +233,7 @@ export default class GoogleCalendarService implements Calendar { async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise { return new Promise(async (resolve, reject) => { - const auth = await this.auth; - const myGoogleAuth = await auth.getToken(); + const myGoogleAuth = await this.auth.getToken(); const calendar = google.calendar({ version: "v3", auth: myGoogleAuth, @@ -295,8 +273,7 @@ export default class GoogleCalendarService implements Calendar { selectedCalendars: IntegrationCalendar[] ): Promise { return new Promise(async (resolve, reject) => { - const auth = await this.auth; - const myGoogleAuth = await auth.getToken(); + const myGoogleAuth = await this.auth.getToken(); const calendar = google.calendar({ version: "v3", auth: myGoogleAuth, @@ -326,22 +303,19 @@ export default class GoogleCalendarService implements Calendar { }, }, (err, apires) => { - if (err) { - reject(err); - } - let result: Prisma.PromiseReturnType = []; - - if (apires?.data.calendars) { - result = Object.values(apires.data.calendars).reduce((c, i) => { - i.busy?.forEach((busyTime) => { - c.push({ - start: busyTime.start || "", - end: busyTime.end || "", - }); + if (err) return reject(err); + // If there's no calendar we just skip + if (!apires?.data.calendars) return resolve([]); + const result = Object.values(apires.data.calendars).reduce((c, i) => { + i.busy?.forEach((busyTime) => { + c.push({ + start: busyTime.start || "", + end: busyTime.end || "", }); - return c; - }, [] as typeof result); - } + }); + return c; + }, [] as Prisma.PromiseReturnType); + resolve(result); } ); @@ -356,8 +330,7 @@ export default class GoogleCalendarService implements Calendar { async listCalendars(): Promise { return new Promise(async (resolve, reject) => { - const auth = await this.auth; - const myGoogleAuth = await auth.getToken(); + const myGoogleAuth = await this.auth.getToken(); const calendar = google.calendar({ version: "v3", auth: myGoogleAuth, diff --git a/packages/app-store/googlecalendar/lib/getGoogleAppKeys.ts b/packages/app-store/googlecalendar/lib/getGoogleAppKeys.ts new file mode 100644 index 0000000000..88873b514d --- /dev/null +++ b/packages/app-store/googlecalendar/lib/getGoogleAppKeys.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import getParsedAppKeysFromSlug from "../../_utils/getParsedAppKeysFromSlug"; + +const googleAppKeysSchema = z.object({ + client_id: z.string(), + client_secret: z.string(), + redirect_uris: z.array(z.string()), +}); + +export const getGoogleAppKeys = async () => { + return getParsedAppKeysFromSlug("google-calendar", googleAppKeysSchema); +}; diff --git a/packages/app-store/googlecalendar/lib/googleCredentialSchema.ts b/packages/app-store/googlecalendar/lib/googleCredentialSchema.ts new file mode 100644 index 0000000000..83cde1913e --- /dev/null +++ b/packages/app-store/googlecalendar/lib/googleCredentialSchema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const googleCredentialSchema = z.object({ + scope: z.string(), + token_type: z.literal("Bearer"), + expiry_date: z.number(), + access_token: z.string(), + refresh_token: z.string(), +});