152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
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) }),
|
|
});
|