2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
---
items:
- 1.png
---
{DESCRIPTION}

View File

@@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring";
import { WEBAPP_URL } from "@calcom/lib/constants";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
let client_id = "";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const appKeys = await getAppKeysFromSlug("zohocrm");
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
if (!client_id) return res.status(400).json({ message: "zohocrm client id missing." });
const state = encodeOAuthState(req);
const params = {
client_id,
response_type: "code",
redirect_uri: `${WEBAPP_URL}/api/integrations/zohocrm/callback`,
scope: ["ZohoCRM.modules.ALL", "ZohoCRM.users.READ", "AaaServer.profile.READ"],
access_type: "offline",
state,
prompt: "consent",
};
const query = stringify(params);
const url = `https://accounts.zoho.com/oauth/v2/auth?${query}`;
res.status(200).json({ url });
}

View File

@@ -0,0 +1,5 @@
import { defaultHandler } from "@calcom/lib/server";
export default defaultHandler({
GET: import("./_getAdd"),
});

View File

@@ -0,0 +1,85 @@
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import qs from "qs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import appConfig from "../config.json";
let client_id = "";
let client_secret = "";
function isAuthorizedAccountsServerUrl(accountsServer: string) {
// As per https://www.zoho.com/crm/developer/docs/api/v6/multi-dc.html#:~:text=US:%20https://accounts.zoho,https://accounts.zohocloud.ca&text=The%20%22location=us%22%20parameter,domain%20in%20all%20API%20endpoints.&text=You%20must%20make%20the%20authorization,.zoho.com.cn.
const authorizedAccountServers = [
"https://accounts.zoho.com",
"https://accounts.zoho.eu",
"https://accounts.zoho.in",
"https://accounts.zoho.com.cn",
"https://accounts.zoho.jp",
"https://accounts.zohocloud.ca",
"https://accounts.zoho.com.au",
];
return authorizedAccountServers.includes(accountsServer);
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code, "accounts-server": accountsServer } = req.query;
if (code === undefined && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
if (!accountsServer || typeof accountsServer !== "string") {
res.status(400).json({ message: "`accounts-server` is required and must be a string" });
return;
}
if (!isAuthorizedAccountsServerUrl(accountsServer)) {
res.status(400).json({ message: "`accounts-server` is not authorized" });
return;
}
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const appKeys = await getAppKeysFromSlug("zohocrm");
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
if (!client_id) return res.status(400).json({ message: "Zoho Crm consumer key missing." });
if (!client_secret) return res.status(400).json({ message: "Zoho Crm consumer secret missing." });
const url = `${accountsServer}/oauth/v2/token`;
const redirectUri = `${WEBAPP_URL}/api/integrations/zohocrm/callback`;
const formData = {
grant_type: "authorization_code",
client_id: client_id,
client_secret: client_secret,
redirect_uri: redirectUri,
code: code,
};
const zohoCrmTokenInfo = await axios({
method: "post",
url: url,
data: qs.stringify(formData),
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
});
// set expiry date as offset from current time.
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
zohoCrmTokenInfo.data.accountServer = accountsServer;
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, zohoCrmTokenInfo.data, req);
const state = decodeOAuthState(req);
res.redirect(
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other", slug: "zohocrm" })
);
}

View File

@@ -0,0 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";

View File

@@ -0,0 +1,27 @@
import { usePathname } from "next/navigation";
import AppCard from "@calcom/app-store/_components/AppCard";
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { WEBAPP_URL } from "@calcom/lib/constants";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const pathname = usePathname();
const { enabled, updateEnabled } = useIsAppEnabled(app);
return (
<AppCard
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
app={app}
teamId={eventType.team?.id || undefined}
switchOnClick={(e) => {
updateEnabled(e);
}}
switchChecked={enabled}
hideAppCardOptions
/>
);
};
export default EventTypeAppCard;

View File

@@ -0,0 +1,18 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "ZohoCRM",
"slug": "zohocrm",
"type": "zohocrm_crm",
"logo": "icon.svg",
"url": "https://github.com/jatinsandilya",
"variant": "crm",
"categories": ["crm"],
"extendsFeature": "EventType",
"publisher": "Jatin Sandilya",
"email": "help@cal.com",
"description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"isOAuth": true
}

View File

@@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";

View File

@@ -0,0 +1,292 @@
import axios from "axios";
import qs from "qs";
import { getLocation } from "@calcom/lib/CalEventParser";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { CRM, Contact, ContactCreateInput } from "@calcom/types/CrmService";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
export type ZohoToken = {
scope: string;
api_domain: string;
expires_in: number;
expiryDate: number;
token_type: string;
access_token: string;
accountServer: string;
refresh_token: string;
};
export type ZohoContact = {
id: string;
email: string;
};
/**
* Converts to the date Format as required by zoho: 2020-08-02T15:30:00+05:30
* https://www.zoho.com/crm/developer/docs/api/v2/events-response.html
*/
const toISO8601String = (date: Date) => {
const tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? "+" : "-",
pad = function (num: number) {
return (num < 10 ? "0" : "") + num;
};
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${dif}${pad(Math.floor(Math.abs(tzo) / 60))}:${pad(
Math.abs(tzo) % 60
)}`;
};
export default class ZohoCrmCrmService implements CRM {
private integrationName = "";
private auth: Promise<{ getToken: () => Promise<void> }>;
private log: typeof logger;
private client_id = "";
private client_secret = "";
private accessToken = "";
constructor(credential: CredentialPayload) {
this.integrationName = "zohocrm_crm";
this.auth = this.zohoCrmAuth(credential).then((r) => r);
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
}
async createContacts(contactsToCreate: ContactCreateInput[]) {
const auth = await this.auth;
await auth.getToken();
const contacts = contactsToCreate.map((contactToCreate) => {
const [firstname, lastname] = !!contactToCreate.name
? contactToCreate.name.split(" ")
: [contactToCreate.email, "-"];
return {
First_Name: firstname,
Last_Name: lastname || "-",
Email: contactToCreate.email,
};
});
const response = await axios({
method: "post",
url: `https://www.zohoapis.com/crm/v3/Contacts`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
data: JSON.stringify({ data: contacts }),
});
const { data } = response;
return data.data.map((contact: ZohoContact) => {
return {
id: contact.id,
email: contact.email,
};
});
}
async getContacts(emails: string | string[]) {
const auth = await this.auth;
await auth.getToken();
const emailsArray = Array.isArray(emails) ? emails : [emails];
const searchCriteria = `(${emailsArray.map((email) => `(Email:equals:${encodeURI(email)})`).join("or")})`;
const response = await axios({
method: "get",
url: `https://www.zohoapis.com/crm/v3/Contacts/search?criteria=${searchCriteria}`,
headers: {
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
})
.then((data) => data.data)
.catch((e) => {
this.log.error(e, e.response?.data);
});
return response
? response.data.map((contact: ZohoContact) => {
return {
id: contact.id,
email: contact.email,
};
})
: [];
}
private getMeetingBody = (event: CalendarEvent): string => {
return `<b>${event.organizer.language.translate("invitee_timezone")}:</b> ${
event.attendees[0].timeZone
}<br><br><b>${event.organizer.language.translate("share_additional_notes")}</b><br>${
event.additionalNotes || "-"
}`;
};
private createZohoEvent = async (event: CalendarEvent, contacts: Contact[]) => {
const zohoEvent = {
Event_Title: event.title,
Start_DateTime: toISO8601String(new Date(event.startTime)),
End_DateTime: toISO8601String(new Date(event.endTime)),
Description: this.getMeetingBody(event),
Venue: getLocation(event),
Who_Id: contacts[0].id, // Link the first attendee as the primary Who_Id
};
return axios({
method: "post",
url: `https://www.zohoapis.com/crm/v3/Events`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
data: JSON.stringify({ data: [zohoEvent] }),
})
.then((data) => data.data)
.catch((e) => this.log.error(e, e.response?.data));
};
private updateMeeting = async (uid: string, event: CalendarEvent) => {
const zohoEvent = {
id: uid,
Event_Title: event.title,
Start_DateTime: toISO8601String(new Date(event.startTime)),
End_DateTime: toISO8601String(new Date(event.endTime)),
Description: this.getMeetingBody(event),
Venue: getLocation(event),
};
return axios({
method: "put",
url: `https://www.zohoapis.com/crm/v3/Events`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
data: JSON.stringify({ data: [zohoEvent] }),
})
.then((data) => data.data)
.catch((e) => this.log.error(e, e.response?.data));
};
private deleteMeeting = async (uid: string) => {
return axios({
method: "delete",
url: `https://www.zohoapis.com/crm/v3/Events?ids=${uid}`,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${this.accessToken}`,
},
})
.then((data) => data.data)
.catch((e) => this.log.error(e, e.response?.data));
};
private zohoCrmAuth = async (credential: CredentialPayload) => {
const appKeys = await getAppKeysFromSlug("zohocrm");
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Zoho CRM client_id missing." });
if (!this.client_secret)
throw new HttpError({ statusCode: 400, message: "Zoho CRM client_secret missing." });
const credentialKey = credential.key as unknown as ZohoToken;
const isTokenValid = (token: ZohoToken) => {
const isValid = token && token.access_token && token.expiryDate && token.expiryDate > Date.now();
if (isValid) {
this.accessToken = token.access_token;
}
return isValid;
};
const refreshAccessToken = async (credentialKey: ZohoToken) => {
try {
const url = `${credentialKey.accountServer}/oauth/v2/token`;
const formData = {
grant_type: "refresh_token",
client_id: this.client_id,
client_secret: this.client_secret,
refresh_token: credentialKey.refresh_token,
};
const zohoCrmTokenInfo = await refreshOAuthTokens(
async () =>
await axios({
method: "post",
url: url,
data: qs.stringify(formData),
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
}),
"zohocrm",
credential.userId
);
if (!zohoCrmTokenInfo.data.error) {
// set expiry date as offset from current time.
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
await prisma.credential.update({
where: {
id: credential.id,
},
data: {
key: {
...(zohoCrmTokenInfo.data as ZohoToken),
refresh_token: credentialKey.refresh_token,
accountServer: credentialKey.accountServer,
},
},
});
this.accessToken = zohoCrmTokenInfo.data.access_token;
this.log.debug("Fetched token", this.accessToken);
} else {
this.log.error(zohoCrmTokenInfo.data);
}
} catch (e: unknown) {
this.log.error(e);
}
};
return {
getToken: () => (isTokenValid(credentialKey) ? Promise.resolve() : refreshAccessToken(credentialKey)),
};
};
async handleEventCreation(event: CalendarEvent, contacts: Contact[]) {
const meetingEvent = await this.createZohoEvent(event, contacts);
if (meetingEvent.data && meetingEvent.data.length && meetingEvent.data[0].status === "success") {
this.log.debug("event:creation:ok", { meetingEvent });
return Promise.resolve({
uid: meetingEvent.data[0].details.id,
id: meetingEvent.data[0].details.id,
type: this.integrationName,
password: "",
url: "",
additionalInfo: { contacts, meetingEvent },
});
}
this.log.debug("meeting:creation:notOk", { meetingEvent, event, contacts });
return Promise.reject("Something went wrong when creating a meeting in ZohoCRM");
}
async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<NewCalendarEventType> {
const auth = await this.auth;
await auth.getToken();
return await this.handleEventCreation(event, contacts);
}
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
const auth = await this.auth;
await auth.getToken();
return await this.updateMeeting(uid, event);
}
async deleteEvent(uid: string): Promise<void> {
const auth = await this.auth;
await auth.getToken();
return await this.deleteMeeting(uid);
}
}

View File

@@ -0,0 +1 @@
export { default as CrmService } from "./CrmService";

View File

@@ -0,0 +1,14 @@
{
"private": true,
"name": "@calcom/zohocrm",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*",
"@calcom/prisma": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,10 @@
import { z } from "zod";
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
export const appKeysSchema = z.object({
client_id: z.string().min(1),
client_secret: z.string().min(1),
});
export const appDataSchema = eventTypeAppCardZod;