first commit
This commit is contained in:
42
calcom/packages/app-store/zohocalendar/api/add.ts
Normal file
42
calcom/packages/app-store/zohocalendar/api/add.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
import config from "../config.json";
|
||||
import { appKeysSchema as zohoKeysSchema } from "../zod";
|
||||
|
||||
const OAUTH_BASE_URL = "https://accounts.zoho.com/oauth/v2";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const appKeys = await getAppKeysFromSlug(config.slug);
|
||||
const { client_id } = zohoKeysSchema.parse(appKeys);
|
||||
|
||||
const state = encodeOAuthState(req);
|
||||
|
||||
const params = {
|
||||
client_id,
|
||||
response_type: "code",
|
||||
redirect_uri: `${WEBAPP_URL}/api/integrations/zohocalendar/callback`,
|
||||
scope: [
|
||||
"ZohoCalendar.calendar.ALL",
|
||||
"ZohoCalendar.event.ALL",
|
||||
"ZohoCalendar.freebusy.READ",
|
||||
"AaaServer.profile.READ",
|
||||
],
|
||||
access_type: "offline",
|
||||
state,
|
||||
prompt: "consent",
|
||||
};
|
||||
|
||||
const query = stringify(params);
|
||||
|
||||
res.status(200).json({ url: `${OAUTH_BASE_URL}/auth?${query}` });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
151
calcom/packages/app-store/zohocalendar/api/callback.ts
Normal file
151
calcom/packages/app-store/zohocalendar/api/callback.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { renewSelectedCalendarCredentialId } from "@calcom/lib/connectedCalendar";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import config from "../config.json";
|
||||
import type { ZohoAuthCredentials } from "../types/ZohoCalendar";
|
||||
import { appKeysSchema as zohoKeysSchema } from "../zod";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: [`[[zohocalendar/api/callback]`] });
|
||||
|
||||
function getOAuthBaseUrl(domain: string): string {
|
||||
return `https://accounts.zoho.${domain}/oauth/v2`;
|
||||
}
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
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" });
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug(config.slug);
|
||||
const { client_id, client_secret } = zohoKeysSchema.parse(appKeys);
|
||||
|
||||
const params = {
|
||||
client_id,
|
||||
grant_type: "authorization_code",
|
||||
client_secret,
|
||||
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(`${getOAuthBaseUrl(server_location || "com")}/token?${query}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
const responseBody = await JSON.parse(await response.text());
|
||||
|
||||
if (!response.ok || responseBody.error) {
|
||||
log.error("get access_token failed", responseBody);
|
||||
return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
|
||||
}
|
||||
|
||||
const key: ZohoAuthCredentials = {
|
||||
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",
|
||||
};
|
||||
|
||||
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}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const data = await calendarResponse.json();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const primaryCalendar = data.calendars.find((calendar: any) => calendar.isdefault);
|
||||
|
||||
if (primaryCalendar.uid) {
|
||||
const credential = await prisma.credential.create({
|
||||
data: {
|
||||
type: config.type,
|
||||
key,
|
||||
userId: req.session.user.id,
|
||||
appId: config.slug,
|
||||
},
|
||||
});
|
||||
const selectedCalendarWhereUnique = {
|
||||
userId: req.session?.user.id,
|
||||
integration: config.type,
|
||||
externalId: primaryCalendar.uid,
|
||||
};
|
||||
// Wrapping in a try/catch to reduce chance of race conditions-
|
||||
// also this improves performance for most of the happy-paths.
|
||||
try {
|
||||
await prisma.selectedCalendar.create({
|
||||
data: {
|
||||
...selectedCalendarWhereUnique,
|
||||
credentialId: credential.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
let errorMessage = "something_went_wrong";
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
// it is possible a selectedCalendar was orphaned, in this situation-
|
||||
// we want to recover by connecting the existing selectedCalendar to the new Credential.
|
||||
if (await renewSelectedCalendarCredentialId(selectedCalendarWhereUnique, credential.id)) {
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ??
|
||||
getInstalledAppPath({ variant: "calendar", slug: config.slug })
|
||||
);
|
||||
return;
|
||||
}
|
||||
// else
|
||||
errorMessage = "account_already_linked";
|
||||
}
|
||||
await prisma.credential.delete({ where: { id: credential.id } });
|
||||
res.redirect(
|
||||
`${
|
||||
getSafeRedirectUrl(state?.onErrorReturnTo) ??
|
||||
getInstalledAppPath({ variant: config.variant, slug: config.slug })
|
||||
}?error=${errorMessage}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: config.variant, slug: config.slug })
|
||||
);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
2
calcom/packages/app-store/zohocalendar/api/index.ts
Normal file
2
calcom/packages/app-store/zohocalendar/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
Reference in New Issue
Block a user