fix: [CAL-3578] [CAL-2733] Zoho calendar issues (#14905)
* fix zohocalender installation * fix func name * fix: [CAL-3578] [CAL-2733] check events in the calendar when checking availability * fix: server location on credentials instead app keys * refactor: remove console log Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --------- Co-authored-by: pritam <pritam.pk36129@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
@@ -18,10 +18,13 @@ 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") {
|
||||
@@ -29,6 +32,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
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" });
|
||||
}
|
||||
@@ -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}`, {
|
||||
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);
|
||||
@@ -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}`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { stringify } from "querystring";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
@@ -16,11 +15,7 @@ import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
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 = "";
|
||||
@@ -42,7 +37,7 @@ export default class ZohoCalendarService implements Calendar {
|
||||
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",
|
||||
@@ -52,7 +47,7 @@ export default class ZohoCalendarService implements Calendar {
|
||||
|
||||
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",
|
||||
@@ -61,10 +56,16 @@ export default class ZohoCalendarService implements Calendar {
|
||||
|
||||
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 },
|
||||
@@ -87,8 +88,7 @@ export default class ZohoCalendarService implements Calendar {
|
||||
|
||||
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: {
|
||||
@@ -101,8 +101,7 @@ export default class ZohoCalendarService implements Calendar {
|
||||
|
||||
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}`,
|
||||
@@ -263,6 +262,37 @@ export default class ZohoCalendarService implements Calendar {
|
||||
);
|
||||
}
|
||||
|
||||
private async getUnavailability(
|
||||
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)
|
||||
.map((event: any) => {
|
||||
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,
|
||||
@@ -299,7 +329,22 @@ export default class ZohoCalendarService implements Calendar {
|
||||
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 = [];
|
||||
@@ -320,6 +365,22 @@ export default class ZohoCalendarService implements Calendar {
|
||||
))
|
||||
);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export type ZohoAuthCredentials = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
server_location: string;
|
||||
};
|
||||
|
||||
export type FreeBusy = {
|
||||
|
||||
Reference in New Issue
Block a user