From fcd06c80e3998762d973266325b6fd2ed352e6a1 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Wed, 20 Nov 2024 18:54:11 -0500 Subject: [PATCH] feat: install gcal + gvideo by default for google signups (#17773) * feat: install gcal + gvideo by default for google signups * add upsertSelectedCalendar * fix type check --- .env.example | 2 +- packages/app-store/googlecalendar/api/add.ts | 5 +- .../app-store/googlecalendar/api/callback.ts | 43 +++--- .../googlecalendar/lib/CalendarService.ts | 11 +- .../app-store/googlecalendar/lib/constants.ts | 7 - .../app-store/googlecalendar/lib/utils.ts | 29 ---- .../features/auth/lib/next-auth-options.ts | 66 ++++++++- packages/lib/constants.ts | 7 + packages/lib/google.ts | 45 ------ packages/lib/server/repository/credential.ts | 9 ++ packages/lib/server/repository/google.ts | 48 ------ .../lib/server/repository/selectedCalendar.ts | 9 +- packages/lib/server/service/google.ts | 138 ++++++++++++++++++ 13 files changed, 249 insertions(+), 170 deletions(-) delete mode 100644 packages/app-store/googlecalendar/lib/constants.ts delete mode 100644 packages/app-store/googlecalendar/lib/utils.ts delete mode 100644 packages/lib/google.ts delete mode 100644 packages/lib/server/repository/google.ts create mode 100644 packages/lib/server/service/google.ts diff --git a/.env.example b/.env.example index d9acc38cfa..83764f27a8 100644 --- a/.env.example +++ b/.env.example @@ -119,7 +119,7 @@ NEXT_PUBLIC_FRESHCHAT_HOST= # Google OAuth credentials # To enable Login with Google you need to: -# 1. Set `GOOGLE_API_CREDENTIALS` above +# 1. Set `GOOGLE_API_CREDENTIALS` below # 2. Set `GOOGLE_LOGIN_ENABLED` to `true` # When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance # @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications diff --git a/packages/app-store/googlecalendar/api/add.ts b/packages/app-store/googlecalendar/api/add.ts index e4e841f19b..941c6cd853 100644 --- a/packages/app-store/googlecalendar/api/add.ts +++ b/packages/app-store/googlecalendar/api/add.ts @@ -1,11 +1,10 @@ import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; -import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants"; +import { GOOGLE_CALENDAR_SCOPES, SCOPE_USERINFO_PROFILE, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState"; -import { SCOPES } from "../lib/constants"; import { getGoogleAppKeys } from "../lib/getGoogleAppKeys"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { @@ -16,7 +15,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const authUrl = oAuth2Client.generateAuthUrl({ access_type: "offline", - scope: SCOPES, + scope: [SCOPE_USERINFO_PROFILE, ...GOOGLE_CALENDAR_SCOPES], // A refresh token is only returned the first time the user // consents to providing access. For illustration purposes, // setting the prompt to 'consent' will force this consent diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index ef7a514c62..6866402664 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -2,18 +2,21 @@ import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar"; -import { WEBAPP_URL, WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants"; +import { + GOOGLE_CALENDAR_SCOPES, + SCOPE_USERINFO_PROFILE, + WEBAPP_URL, + WEBAPP_URL_FOR_OAUTH, +} from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; -import { getAllCalendars, updateProfilePhoto } from "@calcom/lib/google"; import { HttpError } from "@calcom/lib/http-error"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import { CredentialRepository } from "@calcom/lib/server/repository/credential"; -import { GoogleRepository } from "@calcom/lib/server/repository/google"; +import { GoogleService } from "@calcom/lib/server/service/google"; import { Prisma } from "@calcom/prisma/client"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; -import { REQUIRED_SCOPES, SCOPE_USERINFO_PROFILE } from "../lib/constants"; import { getGoogleAppKeys } from "../lib/getGoogleAppKeys"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { @@ -47,7 +50,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const key = token.tokens; const grantedScopes = token.tokens.scope?.split(" ") ?? []; // Check if we have granted all required permissions - const hasMissingRequiredScopes = REQUIRED_SCOPES.some((scope) => !grantedScopes.includes(scope)); + const hasMissingRequiredScopes = GOOGLE_CALENDAR_SCOPES.some((scope) => !grantedScopes.includes(scope)); if (hasMissingRequiredScopes) { if (!state?.fromApp) { throw new HttpError({ @@ -72,19 +75,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { auth: oAuth2Client, }); - const cals = await getAllCalendars(calendar); - - const primaryCal = cals.find((cal) => cal.primary) ?? cals[0]; - - // Only attempt to update the user's profile photo if the user has granted the required scope - if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) { - await updateProfilePhoto(oAuth2Client, req.session.user.id); - } - - const gcalCredential = await GoogleRepository.createGoogleCalendarCredential({ - key, - userId: req.session.user.id, - }); + const primaryCal = await GoogleService.getPrimaryCalendar(calendar); // If we still don't have a primary calendar skip creating the selected calendar. // It can be toggled on later. @@ -96,6 +87,16 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { return; } + // Only attempt to update the user's profile photo if the user has granted the required scope + if (grantedScopes.includes(SCOPE_USERINFO_PROFILE)) { + await GoogleService.updateProfilePhoto(oAuth2Client, req.session.user.id); + } + + const gcalCredential = await GoogleService.createGoogleCalendarCredential({ + key, + userId: req.session.user.id, + }); + const selectedCalendarWhereUnique = { userId: req.session.user.id, externalId: primaryCal.id, @@ -105,7 +106,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { // Wrapping in a try/catch to reduce chance of race conditions- // also this improves performance for most of the happy-paths. try { - await GoogleRepository.upsertSelectedCalendar({ + await GoogleService.upsertSelectedCalendar({ credentialId: gcalCredential.id, externalId: selectedCalendarWhereUnique.externalId, userId: selectedCalendarWhereUnique.userId, @@ -145,7 +146,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { return; } - const existingGoogleMeetCredential = await GoogleRepository.findGoogleMeetCredential({ + const existingGoogleMeetCredential = await GoogleService.findGoogleMeetCredential({ userId: req.session.user.id, }); @@ -159,7 +160,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { } // Create a new google meet credential - await GoogleRepository.createGoogleMeetsCredential({ userId: req.session.user.id }); + await GoogleService.createGoogleMeetsCredential({ userId: req.session.user.id }); res.redirect( getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ?? getInstalledAppPath({ variant: "conferencing", slug: "google-meet" }) diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index e5e328f957..1ee59f1450 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -18,10 +18,9 @@ import { CREDENTIAL_SYNC_SECRET_HEADER_NAME, } from "@calcom/lib/constants"; import { formatCalEvent } from "@calcom/lib/formatCalendarEvent"; -import { getAllCalendars } from "@calcom/lib/google"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; -import { GoogleRepository } from "@calcom/lib/server/repository/google"; +import { GoogleService } from "@calcom/lib/server/service/google"; import prisma from "@calcom/prisma"; import type { Calendar, @@ -543,7 +542,7 @@ export default class GoogleCalendarService implements Calendar { } async function getCalIds() { if (selectedCalendarIds.length !== 0) return selectedCalendarIds; - const cals = await getAllCalendars(calendar, ["id"]); + const cals = await GoogleService.getAllCalendars(calendar, ["id"]); if (!cals.length) return []; return cals.reduce((c, cal) => (cal.id ? [...c, cal.id] : c), [] as string[]); } @@ -607,7 +606,7 @@ export default class GoogleCalendarService implements Calendar { status: 200, statusText: "OK", data: { - items: await getAllCalendars(calendar), + items: await GoogleService.getAllCalendars(calendar), }, }) ); @@ -652,7 +651,7 @@ export default class GoogleCalendarService implements Calendar { }, }); const response = res.data; - await GoogleRepository.upsertSelectedCalendar({ + await GoogleService.upsertSelectedCalendar({ userId: this.credential.userId!, externalId: calendarId, credentialId: this.credential.id, @@ -686,7 +685,7 @@ export default class GoogleCalendarService implements Calendar { .catch((err) => { console.warn(JSON.stringify(err)); }); - await GoogleRepository.upsertSelectedCalendar({ + await GoogleService.upsertSelectedCalendar({ userId: this.credential.userId!, externalId: calendarId, credentialId: this.credential.id, diff --git a/packages/app-store/googlecalendar/lib/constants.ts b/packages/app-store/googlecalendar/lib/constants.ts deleted file mode 100644 index 5c285ce803..0000000000 --- a/packages/app-store/googlecalendar/lib/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const SCOPE_USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile"; -export const SCOPE_CALENDAR_READONLY = "https://www.googleapis.com/auth/calendar.readonly"; -export const SCOPE_CALENDAR_EVENT = "https://www.googleapis.com/auth/calendar.events"; - -export const REQUIRED_SCOPES = [SCOPE_CALENDAR_READONLY, SCOPE_CALENDAR_EVENT]; - -export const SCOPES = [...REQUIRED_SCOPES, SCOPE_USERINFO_PROFILE]; diff --git a/packages/app-store/googlecalendar/lib/utils.ts b/packages/app-store/googlecalendar/lib/utils.ts deleted file mode 100644 index a9436827e9..0000000000 --- a/packages/app-store/googlecalendar/lib/utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { calendar_v3 } from "googleapis"; - -import logger from "@calcom/lib/logger"; - -export async function getAllCalendars( - calendar: calendar_v3.Calendar, - fields: string[] = ["id", "summary", "primary", "accessRole"] -): Promise { - let allCalendars: calendar_v3.Schema$CalendarListEntry[] = []; - let pageToken: string | undefined; - - try { - do { - const response: any = await calendar.calendarList.list({ - fields: `items(${fields.join(",")}),nextPageToken`, - pageToken, - maxResults: 250, // 250 is max - }); - - allCalendars = [...allCalendars, ...(response.data.items ?? [])]; - pageToken = response.data.nextPageToken; - } while (pageToken); - - return allCalendars; - } catch (error) { - logger.error("Error fetching all Google Calendars", { error }); - throw error; - } -} diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 3c51b393c7..00a83fafa5 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -1,5 +1,6 @@ import type { Membership, Team, UserPermissionRole } from "@prisma/client"; import { waitUntil } from "@vercel/functions"; +import { google } from "googleapis"; import type { AuthOptions, Session, User } from "next-auth"; import type { JWT } from "next-auth/jwt"; import { encode } from "next-auth/jwt"; @@ -15,7 +16,12 @@ import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/Imperso import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; -import { HOSTED_CAL_FEATURES, IS_CALCOM } from "@calcom/lib/constants"; +import { + GOOGLE_CALENDAR_SCOPES, + GOOGLE_OAUTH_SCOPES, + HOSTED_CAL_FEATURES, + IS_CALCOM, +} from "@calcom/lib/constants"; import { ENABLE_PROFILE_SWITCHER, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { symmetricDecrypt, symmetricEncrypt } from "@calcom/lib/crypto"; import { defaultCookies } from "@calcom/lib/default-cookies"; @@ -25,6 +31,7 @@ import { randomString } from "@calcom/lib/random"; import { safeStringify } from "@calcom/lib/safeStringify"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import { UserRepository } from "@calcom/lib/server/repository/user"; +import { GoogleService } from "@calcom/lib/server/service/google"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums"; @@ -251,6 +258,13 @@ if (IS_GOOGLE_LOGIN_ENABLED) { clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, allowDangerousEmailAccountLinking: true, + authorization: { + params: { + scope: [...GOOGLE_OAUTH_SCOPES, ...GOOGLE_CALENDAR_SCOPES].join(" "), + access_type: "offline", + prompt: "consent", + }, + }, }) ); } @@ -608,6 +622,55 @@ export const getOptions = ({ return await autoMergeIdentities(); } + const grantedScopes = account.scope?.split(" ") ?? []; + if ( + account.provider === "google" && + !(await GoogleService.findFirstGoogleCalendarCredential({ + userId: user.id as number, + })) && + GOOGLE_CALENDAR_SCOPES.every((scope) => grantedScopes.includes(scope)) + ) { + // Installing Google Calendar by default + const credentialkey = { + access_token: account.access_token, + refresh_token: account.refresh_token, + id_token: account.id_token, + token_type: account.token_type, + expires_at: account.expires_at, + }; + + const gcalCredential = await GoogleService.createGoogleCalendarCredential({ + userId: user.id as number, + key: credentialkey, + }); + + if ( + !(await GoogleService.findGoogleMeetCredential({ + userId: user.id as number, + })) + ) { + await GoogleService.createGoogleMeetsCredential({ + userId: user.id as number, + }); + } + + const oAuth2Client = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); + oAuth2Client.setCredentials(credentialkey); + const calendar = google.calendar({ + version: "v3", + auth: oAuth2Client, + }); + const primaryCal = await GoogleService.getPrimaryCalendar(calendar); + if (primaryCal?.id) { + await GoogleService.createSelectedCalendar({ + credentialId: gcalCredential.id, + userId: user.id as number, + externalId: primaryCal.id, + }); + } + await GoogleService.updateProfilePhoto(oAuth2Client, user.id as number); + } + return { ...token, id: existingUser.id, @@ -632,7 +695,6 @@ export const getOptions = ({ log.debug("callbacks:session - Session callback called", safeStringify({ session, token, user })); const licenseKeyService = await LicenseKeySingleton.getInstance(); const hasValidLicense = await licenseKeyService.checkLicense(); - const profileId = token.profileId; const calendsoSession: Session = { ...session, diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 02e3fe7aac..b1563cd176 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -192,3 +192,10 @@ export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION ? `${WEBAPP_URL}/stop-recording.svg` : `https://app.cal.com/stop-recording.svg`; +export const SCOPE_USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile"; +export const SCOPE_USERINFO_EMAIL = "https://www.googleapis.com/auth/userinfo.email"; +export const GOOGLE_OAUTH_SCOPES = [SCOPE_USERINFO_PROFILE, SCOPE_USERINFO_EMAIL]; +export const GOOGLE_CALENDAR_SCOPES = [ + "https://www.googleapis.com/auth/calendar.events", + "https://www.googleapis.com/auth/calendar.readonly", +]; diff --git a/packages/lib/google.ts b/packages/lib/google.ts deleted file mode 100644 index df9882b559..0000000000 --- a/packages/lib/google.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { OAuth2Client } from "google-auth-library"; -import type { calendar_v3 } from "googleapis"; -import { google } from "googleapis"; - -import logger from "@calcom/lib/logger"; - -import { UserRepository } from "./server/repository/user"; - -export async function getAllCalendars( - calendar: calendar_v3.Calendar, - fields: string[] = ["id", "summary", "primary", "accessRole"] -): Promise { - let allCalendars: calendar_v3.Schema$CalendarListEntry[] = []; - let pageToken: string | undefined; - - try { - do { - const response: any = await calendar.calendarList.list({ - fields: `items(${fields.join(",")}),nextPageToken`, - pageToken, - maxResults: 250, // 250 is max - }); - - allCalendars = [...allCalendars, ...(response.data.items ?? [])]; - pageToken = response.data.nextPageToken; - } while (pageToken); - - return allCalendars; - } catch (error) { - logger.error("Error fetching all Google Calendars", { error }); - throw error; - } -} - -export async function updateProfilePhoto(oAuth2Client: OAuth2Client, userId: number) { - try { - const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client }); - const userDetails = await oauth2.userinfo.get(); - if (userDetails.data?.picture) { - await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture }); - } - } catch (error) { - logger.error("Error updating avatarUrl from google calendar connect", error); - } -} diff --git a/packages/lib/server/repository/credential.ts b/packages/lib/server/repository/credential.ts index e858a3fc67..a4a244a8a7 100644 --- a/packages/lib/server/repository/credential.ts +++ b/packages/lib/server/repository/credential.ts @@ -30,6 +30,15 @@ export class CredentialRepository { }); } + static async findFirstByAppIdAndUserId({ appId, userId }: { appId: string; userId: number }) { + return await prisma.credential.findFirst({ + where: { + appId, + userId, + }, + }); + } + static async findFirstByUserIdAndType({ userId, type }: { userId: number; type: string }) { return await prisma.credential.findFirst({ where: { userId, type } }); } diff --git a/packages/lib/server/repository/google.ts b/packages/lib/server/repository/google.ts deleted file mode 100644 index 571a1b5fa7..0000000000 --- a/packages/lib/server/repository/google.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Prisma } from "@prisma/client"; -import type { Credentials } from "google-auth-library"; - -import { CredentialRepository } from "./credential"; -import { SelectedCalendarRepository } from "./selectedCalendar"; - -export class GoogleRepository { - static async createGoogleCalendarCredential({ userId, key }: { userId: number; key: Credentials }) { - return await CredentialRepository.create({ - type: "google_calendar", - key, - userId, - appId: "google-calendar", - }); - } - - static async createGoogleMeetsCredential({ userId }: { userId: number }) { - return await CredentialRepository.create({ - type: "google_video", - key: {}, - userId, - appId: "google-meet", - }); - } - - static async createSelectedCalendar(data: { credentialId: number; userId: number; externalId: string }) { - return await SelectedCalendarRepository.create({ - ...data, - integration: "google_calendar", - }); - } - - static async upsertSelectedCalendar( - data: Omit - ) { - return await SelectedCalendarRepository.upsert({ - ...data, - integration: "google_calendar", - }); - } - - static async findGoogleMeetCredential({ userId }: { userId: number }) { - return await CredentialRepository.findFirstByUserIdAndType({ - userId, - type: "google_video", - }); - } -} diff --git a/packages/lib/server/repository/selectedCalendar.ts b/packages/lib/server/repository/selectedCalendar.ts index d35fd5de91..e40a4fb087 100644 --- a/packages/lib/server/repository/selectedCalendar.ts +++ b/packages/lib/server/repository/selectedCalendar.ts @@ -3,15 +3,8 @@ import type { Prisma } from "@prisma/client"; import { prisma } from "@calcom/prisma"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -type SelectedCalendarCreateInput = { - credentialId: number; - userId: number; - externalId: string; - integration: string; -}; - export class SelectedCalendarRepository { - static async create(data: SelectedCalendarCreateInput) { + static async create(data: Prisma.SelectedCalendarUncheckedCreateInput) { return await prisma.selectedCalendar.create({ data: { ...data, diff --git a/packages/lib/server/service/google.ts b/packages/lib/server/service/google.ts new file mode 100644 index 0000000000..2b5115e766 --- /dev/null +++ b/packages/lib/server/service/google.ts @@ -0,0 +1,138 @@ +import type { Prisma } from "@prisma/client"; +import type { Credentials } from "google-auth-library"; +import type { OAuth2Client } from "google-auth-library"; +import type { calendar_v3 } from "googleapis"; +import { google } from "googleapis"; + +import logger from "@calcom/lib/logger"; + +import { CredentialRepository } from "../repository/credential"; +import { SelectedCalendarRepository } from "../repository/selectedCalendar"; +import { UserRepository } from "../repository/user"; + +export class GoogleService { + static async createGoogleCalendarCredential({ userId, key }: { userId: number; key: Credentials }) { + return await CredentialRepository.create({ + type: "google_calendar", + key, + userId, + appId: "google-calendar", + }); + } + + static async createGoogleMeetsCredential({ userId }: { userId: number }) { + return await CredentialRepository.create({ + type: "google_video", + key: {}, + userId, + appId: "google-meet", + }); + } + + static async createSelectedCalendar( + data: Omit + ) { + return await SelectedCalendarRepository.create({ + ...data, + integration: "google_calendar", + }); + } + + static async upsertSelectedCalendar( + data: Omit + ) { + return await SelectedCalendarRepository.upsert({ + ...data, + integration: "google_calendar", + }); + } + + static async findGoogleMeetCredential({ userId }: { userId: number }) { + return await CredentialRepository.findFirstByUserIdAndType({ + userId, + type: "google_video", + }); + } + + static async findFirstGoogleCalendarCredential({ userId }: { userId: number }) { + return await CredentialRepository.findFirstByAppIdAndUserId({ + appId: "google-calendar", + userId, + }); + } + + static async getAllCalendars( + calendar: calendar_v3.Calendar, + fields: string[] = ["id", "summary", "primary", "accessRole"] + ): Promise { + let allCalendars: calendar_v3.Schema$CalendarListEntry[] = []; + let pageToken: string | undefined; + + try { + do { + const response: any = await calendar.calendarList.list({ + fields: `items(${fields.join(",")}),nextPageToken`, + pageToken, + maxResults: 250, // 250 is max + }); + + allCalendars = [...allCalendars, ...(response.data.items ?? [])]; + pageToken = response.data.nextPageToken; + } while (pageToken); + + return allCalendars; + } catch (error) { + logger.error("Error fetching all Google Calendars", { error }); + throw error; + } + } + + static async getPrimaryCalendar( + calendar: calendar_v3.Calendar, + fields: string[] = ["id", "summary", "primary", "accessRole"] + ): Promise { + let pageToken: string | undefined; + let firstCalendar: calendar_v3.Schema$CalendarListEntry | undefined; + + try { + do { + const response: any = await calendar.calendarList.list({ + fields: `items(${fields.join(",")}),nextPageToken`, + pageToken, + maxResults: 250, // 250 is max + }); + + const cals = response.data.items ?? []; + const primaryCal = cals.find((cal: calendar_v3.Schema$CalendarListEntry) => cal.primary); + if (primaryCal) { + return primaryCal; + } + + // Store the first calendar in case no primary is found + if (cals.length > 0 && !firstCalendar) { + firstCalendar = cals[0]; + } + + pageToken = response.data.nextPageToken; + } while (pageToken); + + // should not be reached because Google Cal always has a primary cal + return firstCalendar ?? null; + } catch (error) { + logger.error("Error in `getPrimaryCalendar`", { error }); + throw error; + } + } + + static async updateProfilePhoto(oAuth2Client: OAuth2Client, userId: number) { + try { + const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client }); + const userDetails = await oauth2.userinfo.get(); + if (userDetails.data?.picture) { + await UserRepository.updateAvatar({ id: userId, avatarUrl: userDetails.data.picture }); + } + } catch (error) { + logger.error("Error updating avatarUrl from google calendar connect", error); + } + } +}