Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Reduce bundle size via better imports + install gcal + gvideo by default for google signups #17810

Merged
merged 13 commits into from
Dec 6, 2024
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions apps/api/v2/src/ee/calendars/services/gcal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Prisma } from "@prisma/client";
import { Request } from "express";
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";
import { calendar_v3 } from "@googleapis/calendar";
import { z } from "zod";

import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
Expand Down Expand Up @@ -78,7 +79,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {

const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);

const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
return oAuth2Client;
}

Expand Down Expand Up @@ -134,8 +135,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {

oAuth2Client.setCredentials(key);

const calendar = google.calendar({
version: "v3",
const calendar = new calendar_v3.Calendar({
auth: oAuth2Client,
});

Expand Down
3 changes: 0 additions & 3 deletions apps/api/v2/src/ee/gcal/gcal.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { Prisma } from "@prisma/client";
import { Request } from "express";
import { google } from "googleapis";
import { z } from "zod";
Comment on lines -30 to -33
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports weren't being used


import { APPS_READ, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";

Expand Down
4 changes: 2 additions & 2 deletions apps/api/v2/src/modules/apps/services/gcal.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppsRepository } from "@/modules/apps/apps.repository";
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";
import { z } from "zod";

@Injectable()
Expand All @@ -21,7 +21,7 @@ export class GCalService {

const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);

const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
return oAuth2Client;
}
}
5 changes: 4 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"@dub/analytics": "^0.0.15",
"@formkit/auto-animate": "1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@googleapis/admin": "^23.0.0",
"@googleapis/calendar": "^9.7.9",
"@googleapis/oauth2": "^1.0.7",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",
"@next-auth/prisma-adapter": "^1.0.4",
Expand Down Expand Up @@ -85,7 +88,6 @@
"dotenv-cli": "^6.0.0",
"entities": "^4.4.0",
"eslint-config-next": "^13.2.1",
"googleapis": "^84.0.0",
"gray-matter": "^4.0.3",
"handlebars": "^4.7.7",
"ical.js": "^1.4.0",
Expand Down Expand Up @@ -180,6 +182,7 @@
"deasync": "^0.1.30",
"detect-port": "^1.3.0",
"env-cmd": "^10.1.0",
"google-auth-library": "^9.15.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this as a dev dependency because we need to import type Credentials from the lib.

"module-alias": "^2.2.2",
"msw": "^0.42.3",
"node-html-parser": "^6.1.10",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/pages/api/teams/googleworkspace/add.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";
import type { NextApiRequest, NextApiResponse } from "next";

import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
Expand All @@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

// use differnt callback to normal calendar connection
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);

const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/pages/api/teams/googleworkspace/callback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";

Expand Down Expand Up @@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Google client_secret missing." });

const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);

if (!code) {
throw new Error("No code provided");
Expand Down
17 changes: 17 additions & 0 deletions packages/app-store/_utils/oauth/updateProfilePhotoGoogle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { oauth2_v2 } from "@googleapis/oauth2";
import type { OAuth2Client } from "googleapis-common";

import logger from "@calcom/lib/logger";
import { UserRepository } from "@calcom/lib/server/repository/user";

export async function updateProfilePhotoGoogle(oAuth2Client: OAuth2Client, userId: number) {
try {
const oauth2 = new oauth2_v2.Oauth2({ 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);
}
}
9 changes: 4 additions & 5 deletions packages/app-store/googlecalendar/api/add.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { google } from "googleapis";
import { OAuth2Client } from "googleapis-common";
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) {
// Get token from Google Calendar API
const { client_id, client_secret } = await getGoogleAppKeys();
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);

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
Expand Down
67 changes: 39 additions & 28 deletions packages/app-store/googlecalendar/api/callback.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { google } from "googleapis";
import { calendar_v3 } from "@googleapis/calendar";
import { OAuth2Client } from "googleapis-common";
import type { NextApiRequest, NextApiResponse } from "next";

import { updateProfilePhotoGoogle } from "@calcom/app-store/_utils/oauth/updateProfilePhotoGoogle";
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
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 { 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) {
Expand All @@ -40,14 +45,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {

const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;

const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);

if (code) {
const token = await oAuth2Client.getToken(code);
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({
Expand All @@ -63,29 +68,26 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
return;
}

// Set the primary calendar as the first selected calendar

oAuth2Client.setCredentials(key);

const calendar = google.calendar({
version: "v3",
auth: oAuth2Client,
const gcalCredential = await CredentialRepository.create({
userId: req.session.user.id,
key,
appId: "google-calendar",
type: "google_calendar",
});

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 gCalService = new GoogleCalendarService({
...gcalCredential,
user: null,
});

const gcalCredential = await GoogleRepository.createGoogleCalendarCredential({
key,
userId: req.session.user.id,
const calendar = new calendar_v3.Calendar({
auth: oAuth2Client,
});

const primaryCal = await gCalService.getPrimaryCalendar(calendar);

// If we still don't have a primary calendar skip creating the selected calendar.
// It can be toggled on later.
if (!primaryCal?.id) {
Expand All @@ -96,6 +98,11 @@ 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 updateProfilePhotoGoogle(oAuth2Client, req.session.user.id);
}

const selectedCalendarWhereUnique = {
userId: req.session.user.id,
externalId: primaryCal.id,
Expand All @@ -105,10 +112,8 @@ 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({
credentialId: gcalCredential.id,
await gCalService.upsertSelectedCalendar({
externalId: selectedCalendarWhereUnique.externalId,
userId: selectedCalendarWhereUnique.userId,
});
} catch (error) {
let errorMessage = "something_went_wrong";
Expand Down Expand Up @@ -145,8 +150,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
return;
}

const existingGoogleMeetCredential = await GoogleRepository.findGoogleMeetCredential({
const existingGoogleMeetCredential = await CredentialRepository.findFirstByUserIdAndType({
userId: req.session.user.id,
type: "google_video",
});

// If the user already has a google meet credential, there's nothing to do in here
Expand All @@ -159,7 +165,12 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
}

// Create a new google meet credential
await GoogleRepository.createGoogleMeetsCredential({ userId: req.session.user.id });
await CredentialRepository.create({
userId: req.session.user.id,
type: "google_video",
key: {},
appId: "google-meet",
});
res.redirect(
getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ??
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
Expand Down
20 changes: 17 additions & 3 deletions packages/app-store/googlecalendar/lib/CalendarService.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import prismock from "../../../../tests/libs/__mocks__/prisma";
import oAuthManagerMock, { defaultMockOAuthManager } from "../../tests/__mocks__/OAuthManager";
import { googleapisMock, setCredentialsMock } from "./__mocks__/googleapis";
import { adminMock, calendarMock, setCredentialsMock } from "./__mocks__/googleapis";

import { expect, test, vi } from "vitest";
import { expect, test, beforeEach, vi } from "vitest";
import "vitest-fetch-mock";

import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
Expand All @@ -22,7 +22,21 @@ vi.mock("./getGoogleAppKeys", () => ({
redirect_uris: ["http://localhost:3000/api/integrations/googlecalendar/callback"],
}),
}));
googleapisMock.google;

vi.mock("googleapis-common", () => ({
OAuth2Client: vi.fn().mockImplementation(() => ({
setCredentials: setCredentialsMock,
})),
}));
vi.mock("@googleapis/admin", () => adminMock);
vi.mock("@googleapis/calendar", () => calendarMock);

beforeEach(() => {
vi.clearAllMocks();
setCredentialsMock.mockClear();
calendarMock.calendar_v3.Calendar.mockClear();
adminMock.admin_directory_v1.Admin.mockClear();
});

const googleTestCredentialKey = {
scope: "https://www.googleapis.com/auth/calendar.events",
Expand Down
Loading
Loading