first commit
This commit is contained in:
6
calcom/packages/app-store/zohocrm/DESCRIPTION.md
Normal file
6
calcom/packages/app-store/zohocrm/DESCRIPTION.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
items:
|
||||
- 1.png
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
31
calcom/packages/app-store/zohocrm/api/_getAdd.ts
Normal file
31
calcom/packages/app-store/zohocrm/api/_getAdd.ts
Normal 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 });
|
||||
}
|
||||
5
calcom/packages/app-store/zohocrm/api/add.ts
Normal file
5
calcom/packages/app-store/zohocrm/api/add.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
GET: import("./_getAdd"),
|
||||
});
|
||||
85
calcom/packages/app-store/zohocrm/api/callback.ts
Normal file
85
calcom/packages/app-store/zohocrm/api/callback.ts
Normal 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" })
|
||||
);
|
||||
}
|
||||
2
calcom/packages/app-store/zohocrm/api/index.ts
Normal file
2
calcom/packages/app-store/zohocrm/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
@@ -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;
|
||||
18
calcom/packages/app-store/zohocrm/config.json
Normal file
18
calcom/packages/app-store/zohocrm/config.json
Normal 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
|
||||
}
|
||||
2
calcom/packages/app-store/zohocrm/index.ts
Normal file
2
calcom/packages/app-store/zohocrm/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
292
calcom/packages/app-store/zohocrm/lib/CrmService.ts
Normal file
292
calcom/packages/app-store/zohocrm/lib/CrmService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1
calcom/packages/app-store/zohocrm/lib/index.ts
Normal file
1
calcom/packages/app-store/zohocrm/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CrmService } from "./CrmService";
|
||||
14
calcom/packages/app-store/zohocrm/package.json
Normal file
14
calcom/packages/app-store/zohocrm/package.json
Normal 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"
|
||||
}
|
||||
BIN
calcom/packages/app-store/zohocrm/static/1.png
Normal file
BIN
calcom/packages/app-store/zohocrm/static/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
BIN
calcom/packages/app-store/zohocrm/static/icon.png
Normal file
BIN
calcom/packages/app-store/zohocrm/static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
2
calcom/packages/app-store/zohocrm/static/icon.svg
Normal file
2
calcom/packages/app-store/zohocrm/static/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.2 KiB |
10
calcom/packages/app-store/zohocrm/zod.ts
Normal file
10
calcom/packages/app-store/zohocrm/zod.ts
Normal 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;
|
||||
Reference in New Issue
Block a user