feat: Updated Huddle01 App Flow (#16907)

* fix: migrated to new flow

* feat: apis added

* fix: meetingId

* fix: working with apis

* fix: appName

* fix: WEB APP URL

* fix: update url

* fix: remove logs

* feat: remove lock file

* fix: lock file

* fix: override buildErrors

* fix: removed lock file

* fix: migrated to new flow

* feat: apis added

* fix: meetingId

* fix: working with apis

* fix: appName

* fix: WEB APP URL

* fix: update url

* fix: remove logs

* feat: remove lock file

* fix: lock file

* fix: override buildErrors

* fix: yarn install, lock file

* temp: debugging issue

* feat: api updated

* fix: updated endpoints

* fix: reverted changes

* revert: yarn.lock

* feat: readme

* Update yarn.lock remove spacing

* fix: spacing

---------

Co-authored-by: Akash Mondal <akmo3901@gmail.com>
This commit is contained in:
Ponikar
2024-10-22 11:50:16 +00:00
committed by GitHub
co-authored by Akash Mondal
parent c2662638a5
commit 9382b862e4
10 changed files with 278 additions and 89 deletions
+5
View File
@@ -5,6 +5,7 @@
# - DAILY.CO VIDEO
# - GOOGLE CALENDAR/MEET/LOGIN
# - HUBSPOT
# - HUDDLE01
# - OFFICE 365
# - SLACK
# - STRIPE
@@ -137,3 +138,7 @@ REVERT_PUBLIC_TOKEN=
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
REVERT_API_URL=https://api.revert.dev/
# *********************************************************************************************************
# - Huddle01
# Used for the huddle01 integration
HUDDLE01_API_TOKEN=
@@ -0,0 +1,5 @@
### Obtaining Huddle01 API Token
- As of now, we haven't made OAuth authentication support public.
- Please contact us at support@huddle01.com with the subject line "Require Huddle01 API Token" and mention - details like app name and purpose of token.
- We will share token details via email.
@@ -1,4 +1,3 @@
import { randomString } from "@calcom/lib/random";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@@ -9,7 +8,7 @@ export const metadata = {
installed: true,
type: "huddle01_video",
variant: "conferencing",
categories: ["conferencing"],
categories: ["video", "conferencing"],
logo: "icon.svg",
publisher: "huddle01.com",
url: "https://huddle01.com",
@@ -21,11 +20,10 @@ export const metadata = {
appData: {
location: {
linkType: "dynamic",
type: "integrations:huddle01",
type: "integrations:huddle01_video",
label: "Huddle01 Video",
},
},
key: { apikey: randomString(12) },
dirName: "huddle01video",
concurrentMeetings: true,
isOAuth: false,
+21 -48
View File
@@ -1,54 +1,27 @@
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextApiRequest } from "next";
import { stringify } from "querystring";
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
import prisma from "@calcom/prisma";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
/**
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const { teamId, returnTo } = req.query;
async function handler(req: NextApiRequest) {
const state = encodeOAuthState(req);
await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: req.session.user.id });
const installForObject = teamId ? { teamId: Number(teamId) } : { userId: req.session.user.id };
const params = {
response_type: "code",
redirect_uri: `${WEBAPP_URL}/api/integrations/huddle01video/callback`,
state,
appName: "Calcom",
};
const query = stringify(params);
const appType = "huddle01_video";
try {
const alreadyInstalled = await prisma.credential.findFirst({
where: {
type: appType,
...installForObject,
},
});
if (alreadyInstalled) {
throw new Error("Already installed");
}
const installation = await prisma.credential.create({
data: {
type: appType,
key: {},
...installForObject,
appId: "huddle01",
},
});
if (!installation) {
throw new Error("Unable to create user credential for huddle01video");
}
} catch (error: unknown) {
if (error instanceof Error) {
return res.status(500).json({ message: error.message });
}
return res.status(500);
}
// need to return a json object with the response status
return res
.status(200)
.json({ url: returnTo ?? getInstalledAppPath({ variant: "conferencing", slug: "huddle01" }) });
const url = `https://huddle01.app/thirdparty_auth?${query}`;
return { url };
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import { storeHuddle01Credential } from "../utils/storage";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession({ req, res });
const state = decodeOAuthState(req);
if (!session) {
return res.status(401).json({ message: "Unauthorized" });
}
const userId = session.user.id;
const { identityToken } = req.query;
const token = Array.isArray(identityToken) ? identityToken[0] : identityToken;
if (!token) {
return res.status(401).json({
message: "Something went wrong",
});
}
await storeHuddle01Credential(userId, token);
res.redirect(
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "conferencing", slug: "huddle01" })
);
}
@@ -1 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";
@@ -1,46 +1,143 @@
import z from "zod";
import { handleErrorsJson } from "@calcom/lib/errors";
import { randomString } from "@calcom/lib/random";
import logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
import type { VideoApiAdapter } from "@calcom/types/VideoApiAdapter";
const huddle01Schema = z.object({ url: z.string().url(), roomId: z.string() });
import { getHuddle01APIKey, getHuddle01Credential } from "../utils/storage";
const Huddle01VideoApiAdapter = (): VideoApiAdapter => {
const API_END_POINT = "https://platform-api.huddle01.workers.dev/api/v2/calendar";
const fetchHuddleAPI = async (userId: number) => {
const { identityToken } = await getHuddle01Credential(userId);
const { apiKey } = await getHuddle01APIKey();
const headers = {
"x-api-key": apiKey,
"x-identity-token": identityToken,
};
return (
endpoint: "subdomains" | "createMeeting" | "deleteMeeting" | "updateMeeting",
option?: RequestInit
) => {
return fetch(`${API_END_POINT}/${endpoint}`, {
...option,
headers: {
...option?.headers,
...headers,
},
});
};
};
const log = logger.getSubLogger({ prefix: ["app-store/huddle01video/lib/VideoApiAdapter"] });
const Huddle01ApiAdapter = (credential: CredentialPayload): VideoApiAdapter => {
return {
getAvailability: () => {
return Promise.resolve([]);
},
createMeeting: async (): Promise<VideoCallData> => {
const res = await fetch(
"https://wpss2zlpb9.execute-api.us-east-1.amazonaws.com/new-meeting?utmCampaign=cal.com&utmSource=partner&utmMedium=calendar"
);
const json = await handleErrorsJson<{ url: string }>(res);
const { url } = huddle01Schema.parse(json);
if (url) {
return Promise.resolve({
type: "huddle01_video",
id: randomString(21),
password: "",
url,
});
createMeeting: async (e: CalendarEvent) => {
if (!credential.userId) {
log.error("[Huddle01 Error] -> User is not logged in");
throw new Error("User is not logged in");
}
try {
const fetch = await fetchHuddleAPI(credential.userId);
const res = await fetch("createMeeting", {
method: "POST",
body: JSON.stringify({
title: e.title,
startTime: e.startTime,
endTime: e.endTime,
}),
headers: {
"Content-Type": "application/json",
},
});
const data = (await res.json()) as {
roomId: string;
meetingLink: string;
};
return {
type: "huddle01_video",
id: data.roomId,
password: "",
url: data.meetingLink,
};
} catch (e) {
log.error("[Huddle01 Error] -> Error while creating meeeting", e);
throw Error("Error while creating meeting");
}
return Promise.reject("Url was not received in response body.");
},
deleteMeeting: async (): Promise<void> => {
Promise.resolve();
},
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
return Promise.resolve({
type: "huddle01_video",
id: bookingRef.meetingId as string,
password: bookingRef.meetingPassword as string,
url: bookingRef.meetingUrl as string,
updateMeeting: async (bookingRef: PartialReference, e: CalendarEvent) => {
if (!credential.userId) {
log.error("[Huddle01 Error] -> User is not logged in");
throw new Error("User is not logged in");
}
const fetch = await fetchHuddleAPI(credential.userId);
const res = await fetch("updateMeeting", {
method: "PUT",
body: JSON.stringify({
title: e.title,
startTime: e.startTime,
endTime: e.endTime,
meetingId: bookingRef.uid,
}),
headers: {
"Content-Type": "application/json",
},
});
const data = (await res.json()) as {
roomId: string;
meetingLink: string;
};
return {
type: "huddle01_video",
id: data.roomId,
password: "",
url: data.meetingLink,
};
},
deleteMeeting: async (meetingId: string) => {
if (!credential.userId) {
log.error("[Huddle01 Error] -> User is not logged in");
throw new Error("User is not logged in");
}
const fetch = await fetchHuddleAPI(credential.userId);
const res = await fetch("deleteMeeting", {
method: "DELETE",
body: JSON.stringify({
meetingId,
}),
headers: {
"Content-Type": "application/json",
},
});
const data = (await res.json()) as {
roomId: string;
meetingLink: string;
};
return {
type: "huddle01_video",
id: data.roomId,
password: "",
url: data.meetingLink,
};
},
getAvailability: async () => {
return [];
},
};
};
export default Huddle01VideoApiAdapter;
export default Huddle01ApiAdapter;
@@ -0,0 +1,69 @@
import { z } from "zod";
import { prisma } from "@calcom/prisma";
const huddle01AppKeySchema = z.object({
identityToken: z.string(),
});
export async function storeHuddle01Credential(userId: number, identityToken: string) {
const existingCredential = await prisma.credential.findFirst({
where: {
type: "huddle01_video",
userId: userId,
appId: "huddle01",
},
});
if (existingCredential) {
// Update the existing credential with the new identityToken
await prisma.credential.update({
where: { id: existingCredential.id },
data: { key: { identityToken } },
});
} else {
// Create a new credential if it doesn't exist
await prisma.credential.create({
data: {
type: "huddle01_video",
key: { identityToken },
userId: userId,
appId: "huddle01",
},
});
}
}
export async function getHuddle01Credential(userId: number) {
const credential = await prisma.credential.findFirst({
where: {
type: "huddle01_video",
userId: userId,
appId: "huddle01",
},
});
if (!credential) {
throw new Error("Huddle01 credential not found");
}
return huddle01AppKeySchema.parse(credential?.key);
}
const huddle01APIKeySchema = z.object({
apiKey: z.string(),
});
export async function getHuddle01APIKey() {
const appCreds = await prisma.app.findFirst({
where: {
slug: "huddle01",
},
});
if (!appCreds) {
throw new Error("Huddle01 app credential not found");
}
return huddle01APIKeySchema.parse(appCreds?.keys);
}
+6 -1
View File
@@ -359,7 +359,12 @@ export default async function main() {
await createApp("make", "make", ["automation"], "make_automation", {
invite_link: "https://make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510",
});
await createApp("huddle01", "huddle01video", ["conferencing"], "huddle01_video");
if (process.env.HUDDLE01_API_TOKEN) {
await createApp("huddle01", "huddle01video", ["conferencing"], "huddle01_video", {
apiKey: process.env.HUDDLE01_API_TOKEN,
});
}
// Payment apps
if (
+2 -1
View File
@@ -454,6 +454,7 @@
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
"NEXT_PUBLIC_API_V2_ROOT_URL",
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
"VAPID_PRIVATE_KEY"
"VAPID_PRIVATE_KEY",
"HUDDLE01_API_TOKEN"
]
}