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

fix: [CAL-3578] [CAL-2733] Zoho calendar issues #14905

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 19 additions & 5 deletions packages/app-store/zohocalendar/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@ import { appKeysSchema as zohoKeysSchema } from "../zod";

const log = logger.getSubLogger({ prefix: [`[[zohocalendar/api/callback]`] });

const OAUTH_BASE_URL = "https://accounts.zoho.com/oauth/v2";
function getOAuthBaseUrl(domain: string): string {
return `https://accounts.zoho.${domain}/oauth/v2`;
}

async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
const { code, location } = req.query;

const state = decodeOAuthState(req);

if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}

if (location && typeof location !== "string") {
res.status(400).json({ message: "`location` must be a string" });
return;
}

if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
Expand All @@ -43,17 +51,18 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
redirect_uri: `${WEBAPP_URL}/api/integrations/${config.slug}/callback`,
code,
};
const server_location = location === "us" ? "com" : location;

const query = stringify(params);

const response = await fetch(`${OAUTH_BASE_URL}/token?${query}`, {
const response = await fetch(`${getOAuthBaseUrl(server_location || "com")}/token?${query}`, {
vachmara marked this conversation as resolved.
Show resolved Hide resolved
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});

const responseBody = await response.json();
const responseBody = await JSON.parse(await response.text());

if (!response.ok || responseBody.error) {
log.error("get access_token failed", responseBody);
Expand All @@ -64,9 +73,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
access_token: responseBody.access_token,
refresh_token: responseBody.refresh_token,
expires_in: Math.round(+new Date() / 1000 + responseBody.expires_in),
server_location: server_location || "com",
};

const calendarResponse = await fetch("https://calendar.zoho.com/api/v1/calendars", {
function getCalenderUri(domain: string): string {
return `https://calendar.zoho.${domain}/api/v1/calendars`;
}

const calendarResponse = await fetch(getCalenderUri(server_location || "com"), {
method: "GET",
headers: {
Authorization: `Bearer ${key.access_token}`,
Expand Down
87 changes: 74 additions & 13 deletions packages/app-store/zohocalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { stringify } from "querystring";
import { z } from "zod";

import dayjs from "@calcom/dayjs";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
Expand All @@ -16,11 +15,7 @@

import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import type { ZohoAuthCredentials, FreeBusy, ZohoCalendarListResp } from "../types/ZohoCalendar";

const zohoKeysSchema = z.object({
client_id: z.string(),
client_secret: z.string(),
});
import { appKeysSchema as zohoKeysSchema } from "../zod";

export default class ZohoCalendarService implements Calendar {
private integrationName = "";
Expand All @@ -42,7 +37,7 @@
try {
const appKeys = await getAppKeysFromSlug("zohocalendar");
const { client_id, client_secret } = zohoKeysSchema.parse(appKeys);

const server_location = zohoCredentials.server_location;
const params = {
client_id,
grant_type: "refresh_token",
Expand All @@ -52,7 +47,7 @@

const query = stringify(params);

const res = await fetch(`https://accounts.zoho.com/oauth/v2/token?${query}`, {
const res = await fetch(`https://accounts.zoho.${server_location}/oauth/v2/token?${query}`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
Expand All @@ -61,10 +56,16 @@

const token = await res.json();

// Revert if access_token is not present
if (!token.access_token) {
throw new Error("Invalid token response");
}

const key: ZohoAuthCredentials = {
access_token: token.access_token,
refresh_token: zohoCredentials.refresh_token,
expires_in: Math.round(+new Date() / 1000 + token.expires_in),
server_location,
};
await prisma.credential.update({
where: { id: credential.id },
Expand All @@ -87,8 +88,7 @@

private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const credentials = await this.auth.getToken();

return fetch(`https://calendar.zoho.com/api/v1${endpoint}`, {
return fetch(`https://calendar.zoho.${credentials.server_location}/api/v1${endpoint}`, {
method: "GET",
...init,
headers: {
Expand All @@ -101,8 +101,7 @@

private getUserInfo = async () => {
const credentials = await this.auth.getToken();

const response = await fetch(`https://accounts.zoho.com/oauth/user/info`, {
const response = await fetch(`https://accounts.zoho.${credentials.server_location}/oauth/user/info`, {
method: "GET",
headers: {
Authorization: `Bearer ${credentials.access_token}`,
Expand Down Expand Up @@ -263,6 +262,37 @@
);
}

private async getUnavailability(
vachmara marked this conversation as resolved.
Show resolved Hide resolved
range: { start: string; end: string },
calendarId: string
): Promise<Array<{ start: string; end: string }>> {
const query = stringify({
range: JSON.stringify(range),
});
this.log.debug("getUnavailability query", query);
try {
// List all events within the range
const response = await this.fetcher(`/calendars/${calendarId}/events?${query}`);
const data = await this.handleData(response, this.log);

// Check for no data scenario
if (!data.events || data.events.length === 0) return [];

return (
data.events
.filter((event: any) => event.isprivate === false)

Check warning on line 283 in packages/app-store/zohocalendar/lib/CalendarService.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app-store/zohocalendar/lib/CalendarService.ts#L283

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
vachmara marked this conversation as resolved.
Show resolved Hide resolved
.map((event: any) => {

Check warning on line 284 in packages/app-store/zohocalendar/lib/CalendarService.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app-store/zohocalendar/lib/CalendarService.ts#L284

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
const start = dayjs(event.dateandtime.start, "YYYYMMDD[T]HHmmssZ").utc().toISOString();
const end = dayjs(event.dateandtime.end, "YYYYMMDD[T]HHmmssZ").utc().toISOString();
return { start, end };
}) || []
);
} catch (error) {
this.log.error(error);
return [];
}
}

async getAvailability(
dateFrom: string,
dateTo: string,
Expand Down Expand Up @@ -299,7 +329,22 @@
originalEndDate.format("YYYYMMDD[T]HHmmss[Z]"),
userInfo.Email
);
return busyData;

const unavailabilityData = await Promise.all(
queryIds.map((calendarId) =>
this.getUnavailability(
{
start: originalStartDate.format("YYYYMMDD[T]HHmmss[Z]"),
end: originalEndDate.format("YYYYMMDD[T]HHmmss[Z]"),
},
calendarId
)
)
);

const unavailability = unavailabilityData.flat();

return busyData.concat(unavailability);
} else {
// Zoho only supports 31 days of freebusy data
const busyData = [];
Expand All @@ -320,6 +365,22 @@
))
);

const unavailabilityData = await Promise.all(
queryIds.map((calendarId) =>
this.getUnavailability(
{
start: startDate.format("YYYYMMDD[T]HHmmss[Z]"),
end: endDate.format("YYYYMMDD[T]HHmmss[Z]"),
},
calendarId
)
)
);

const unavailability = unavailabilityData.flat();

busyData.push(...unavailability);

startDate = endDate.add(1, "minutes");
endDate = startDate.add(30, "days");
}
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/zohocalendar/types/ZohoCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type ZohoAuthCredentials = {
access_token: string;
refresh_token: string;
expires_in: number;
server_location: string;
};

export type FreeBusy = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type SaveKeysOptions = {
};

export const saveKeysHandler = async ({ ctx, input }: SaveKeysOptions) => {
console.log(ctx);
vachmara marked this conversation as resolved.
Show resolved Hide resolved
const keysSchema = appKeysSchemas[input.dirName as keyof typeof appKeysSchemas];
const keys = keysSchema.parse(input.keys);

Expand Down