Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into domain-wide-delegatio…
Browse files Browse the repository at this point in the history
…n-google-calendar
  • Loading branch information
hariombalhara committed Sep 15, 2024
2 parents 18a144d + 9bfd933 commit 9a26de9
Show file tree
Hide file tree
Showing 323 changed files with 8,086 additions and 3,086 deletions.
28 changes: 0 additions & 28 deletions .github/workflows/pr-review.yml

This file was deleted.

5 changes: 3 additions & 2 deletions apps/api/v1/pages/api/slots/_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
isColdStart = false;
}

const { usernameList, ...rest } = req.query;
const { usernameList, isTeamEvent, ...rest } = req.query;
const parsedIsTeamEvent = String(isTeamEvent).toLowerCase() === "true";
let slugs = usernameList;
if (!Array.isArray(usernameList)) {
slugs = usernameList ? [usernameList] : undefined;
}
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest });
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
const slotsInProvidedTimeZone = timeZoneSupported
Expand Down
1 change: 1 addition & 0 deletions apps/api/v1/pages/api/teams/_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ const generateTeamCheckoutSession = async ({
metadata: {
pendingPaymentTeamId,
ownerId,
dubCustomerId: ownerId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/api/v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/[email protected].33",
"@calcom/platform-libraries": "npm:@calcom/[email protected].34",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/[email protected]",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
Expand Down
71 changes: 58 additions & 13 deletions apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";

import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries";
import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries";
import { Credential } from "@calcom/prisma/client";

@Injectable()
export class AppleCalendarService implements CredentialSyncCalendarApp {
Expand Down Expand Up @@ -61,26 +62,70 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
if (username.length <= 1 || password.length <= 1)
throw new BadRequestException(`Username or password cannot be empty`);

const data = {
type: APPLE_CALENDAR_TYPE,
key: symmetricEncrypt(
JSON.stringify({ username, password }),
process.env.CALENDSO_ENCRYPTION_KEY || ""
),
userId: userId,
teamId: null,
appId: APPLE_CALENDAR_ID,
invalid: false,
};
const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId(
APPLE_CALENDAR_TYPE,
userId
);

let hasMatchingUsernameAndPassword = false;

if (existingAppleCalendarCredentials.length > 0) {
const hasCalendarWithGivenCredentials = existingAppleCalendarCredentials.find(
(calendarCredential: Credential) => {
const decryptedKey = JSON.parse(
symmetricDecrypt(calendarCredential.key as string, process.env.CALENDSO_ENCRYPTION_KEY || "")
);

if (decryptedKey.username === username) {
if (decryptedKey.password === password) {
hasMatchingUsernameAndPassword = true;
}

return true;
}
}
);

if (!!hasCalendarWithGivenCredentials && hasMatchingUsernameAndPassword) {
return {
status: SUCCESS_STATUS,
};
}

if (!!hasCalendarWithGivenCredentials && !hasMatchingUsernameAndPassword) {
await this.credentialRepository.upsertAppCredential(
APPLE_CALENDAR_TYPE,
symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
userId,
hasCalendarWithGivenCredentials.id
);

return {
status: SUCCESS_STATUS,
};
}
}

try {
const data = {
type: APPLE_CALENDAR_TYPE,
key: symmetricEncrypt(
JSON.stringify({ username, password }),
process.env.CALENDSO_ENCRYPTION_KEY || ""
),
userId: userId,
teamId: null,
appId: APPLE_CALENDAR_ID,
invalid: false,
};

const dav = new CalendarService({
id: 0,
...data,
user: { email: userEmail },
});
await dav?.listCalendars();
await this.credentialRepository.createAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
await this.credentialRepository.upsertAppCredential(APPLE_CALENDAR_TYPE, data.key, userId);
} catch (reason) {
throw new BadRequestException(`Could not add this apple calendar account: ${reason}`);
}
Expand Down
34 changes: 33 additions & 1 deletion apps/api/v2/src/ee/calendars/services/calendars.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@/modules/credentials/credentials.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import {
Injectable,
Expand All @@ -15,9 +16,11 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { User } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { DateTime } from "luxon";
import { z } from "zod";

import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
Expand All @@ -33,7 +36,8 @@ export class CalendarsService {
private readonly calendarsRepository: CalendarsRepository,
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private readonly config: ConfigService
private readonly config: ConfigService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}

async getCalendars(userId: number) {
Expand Down Expand Up @@ -145,4 +149,32 @@ export class CalendarsService {
throw new NotFoundException("Calendar credentials not found");
}
}

async createAndLinkCalendarEntry(
userId: number,
externalId: string,
key: Prisma.InputJsonValue,
calendarType: keyof typeof APPS_TYPE_ID_MAPPING,
credentialId?: number | null
) {
const credential = await this.credentialsRepository.upsertAppCredential(
calendarType,
key,
userId,
credentialId
);

await this.selectedCalendarsRepository.upsertSelectedCalendar(
externalId,
credential.id,
userId,
calendarType
);
}

async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) {
const credential = await this.credentialsRepository.getUserCredentialById(userId, credentialId, type);

return !credential?.invalid;
}
}
40 changes: 32 additions & 8 deletions apps/api/v2/src/ee/calendars/services/gcal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ export class GoogleCalendarService implements OAuthCalendarApp {
const token = await oAuth2Client.getToken(parsedCode);
// Google oAuth Credentials are stored in token.tokens
const key = token.tokens;
const credential = await this.credentialRepository.createAppCredential(
GOOGLE_CALENDAR_TYPE,
key as Prisma.InputJsonValue,
ownerId
);

oAuth2Client.setCredentials(key);

Expand All @@ -149,10 +144,39 @@ export class GoogleCalendarService implements OAuthCalendarApp {
const primaryCal = cals.data.items?.find((cal) => cal.primary);

if (primaryCal?.id) {
await this.selectedCalendarsRepository.createSelectedCalendar(
primaryCal.id,
credential.id,
const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar(
ownerId,
GOOGLE_CALENDAR_TYPE,
primaryCal.id
);

if (alreadyExistingSelectedCalendar) {
const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity(
ownerId,
alreadyExistingSelectedCalendar.credentialId ?? 0,
GOOGLE_CALENDAR_TYPE
);

// user credential probably got expired in this case
if (!isCredentialValid) {
await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
alreadyExistingSelectedCalendar.externalId,
key as Prisma.InputJsonValue,
GOOGLE_CALENDAR_TYPE,
alreadyExistingSelectedCalendar.credentialId
);
}

return {
url: redir || origin,
};
}

await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
primaryCal.id,
key as Prisma.InputJsonValue,
GOOGLE_CALENDAR_TYPE
);
}
Expand Down
35 changes: 29 additions & 6 deletions apps/api/v2/src/ee/calendars/services/outlook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,39 @@ export class OutlookService implements OAuthCalendarApp {
const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token);

if (defaultCalendar?.id) {
const credential = await this.credentialRepository.createAppCredential(
const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar(
ownerId,
OFFICE_365_CALENDAR_TYPE,
office365OAuthCredentials,
ownerId
defaultCalendar.id
);

await this.selectedCalendarsRepository.createSelectedCalendar(
defaultCalendar.id,
credential.id,
if (alreadyExistingSelectedCalendar) {
const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity(
ownerId,
alreadyExistingSelectedCalendar.credentialId ?? 0,
OFFICE_365_CALENDAR_TYPE
);

// user credential probably got expired in this case
if (!isCredentialValid) {
await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
alreadyExistingSelectedCalendar.externalId,
office365OAuthCredentials,
OFFICE_365_CALENDAR_TYPE,
alreadyExistingSelectedCalendar.credentialId
);
}

return {
url: redir || origin,
};
}

await this.calendarsService.createAndLinkCalendarEntry(
ownerId,
defaultCalendar.id,
office365OAuthCredentials,
OFFICE_365_CALENDAR_TYPE
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";

export enum TemplateType {
CHECK_IN_APPOINTMENT = "CHECK_IN_APPOINTMENT",
CUSTOM_TEMPLATE = "CUSTOM_TEMPLATE",
}

export class CreatePhoneCallInput {
@IsString()
@Matches(/^\+[1-9]\d{1,14}$/, {
message:
"Invalid phone number format. Expected format: +<CountryCode><PhoneNumber> with no spaces or separators.",
})
@DocsProperty({ description: "Your phone number" })
yourPhoneNumber!: string;

@IsString()
@Matches(/^\+[1-9]\d{1,14}$/, {
message:
"Invalid phone number format. Expected format: +<CountryCode><PhoneNumber> with no spaces or separators.",
})
@DocsProperty({ description: "Number to call" })
numberToCall!: string;

@IsString()
@DocsProperty({ description: "CAL API Key" })
calApiKey!: string;

@IsBoolean()
@DocsProperty({ description: "Enabled status", default: true })
enabled = true;

@IsEnum(TemplateType)
@DocsProperty({ description: "Template type", enum: TemplateType })
templateType: TemplateType = TemplateType.CUSTOM_TEMPLATE;

@IsOptional()
@IsString()
@DocsProperty({ description: "Scheduler name" })
schedulerName?: string;

@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest name" })
guestName?: string;

@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest email" })
guestEmail?: string;

@IsOptional()
@IsString()
@Transform(({ value }) => (value ? value : undefined))
@DocsProperty({ description: "Guest company" })
guestCompany?: string;

@IsOptional()
@IsString()
@DocsProperty({ description: "Begin message" })
beginMessage?: string;

@IsOptional()
@IsString()
@DocsProperty({ description: "General prompt" })
generalPrompt?: string;
}
Loading

0 comments on commit 9a26de9

Please sign in to comment.