Skip to content

Commit 9382b86

Browse files
ponikarAkMo3
andauthored
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>
1 parent c266263 commit 9382b86

File tree

10 files changed

+275
-86
lines changed

10 files changed

+275
-86
lines changed

.env.appStore.example

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# - DAILY.CO VIDEO
66
# - GOOGLE CALENDAR/MEET/LOGIN
77
# - HUBSPOT
8+
# - HUDDLE01
89
# - OFFICE 365
910
# - SLACK
1011
# - STRIPE
@@ -137,3 +138,7 @@ REVERT_PUBLIC_TOKEN=
137138
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
138139
REVERT_API_URL=https://api.revert.dev/
139140
# *********************************************************************************************************
141+
142+
# - Huddle01
143+
# Used for the huddle01 integration
144+
HUDDLE01_API_TOKEN=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### Obtaining Huddle01 API Token
2+
3+
- As of now, we haven't made OAuth authentication support public.
4+
- 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.
5+
- We will share token details via email.

packages/app-store/huddle01video/_metadata.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { randomString } from "@calcom/lib/random";
21
import type { AppMeta } from "@calcom/types/App";
32

43
import _package from "./package.json";
@@ -9,7 +8,7 @@ export const metadata = {
98
installed: true,
109
type: "huddle01_video",
1110
variant: "conferencing",
12-
categories: ["conferencing"],
11+
categories: ["video", "conferencing"],
1312
logo: "icon.svg",
1413
publisher: "huddle01.com",
1514
url: "https://huddle01.com",
@@ -21,11 +20,10 @@ export const metadata = {
2120
appData: {
2221
location: {
2322
linkType: "dynamic",
24-
type: "integrations:huddle01",
23+
type: "integrations:huddle01_video",
2524
label: "Huddle01 Video",
2625
},
2726
},
28-
key: { apikey: randomString(12) },
2927
dirName: "huddle01video",
3028
concurrentMeetings: true,
3129
isOAuth: false,
+21-48
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,27 @@
1-
import type { NextApiRequest, NextApiResponse } from "next";
1+
import type { NextApiRequest } from "next";
2+
import { stringify } from "querystring";
23

3-
import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam";
4-
import prisma from "@calcom/prisma";
4+
import { WEBAPP_URL } from "@calcom/lib/constants";
5+
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
56

6-
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
7+
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
78

8-
/**
9-
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
10-
* @param req
11-
* @param res
12-
*/
13-
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
14-
if (!req.session?.user?.id) {
15-
return res.status(401).json({ message: "You must be logged in to do this" });
16-
}
17-
const { teamId, returnTo } = req.query;
9+
async function handler(req: NextApiRequest) {
10+
const state = encodeOAuthState(req);
1811

19-
await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: req.session.user.id });
20-
const installForObject = teamId ? { teamId: Number(teamId) } : { userId: req.session.user.id };
12+
const params = {
13+
response_type: "code",
14+
redirect_uri: `${WEBAPP_URL}/api/integrations/huddle01video/callback`,
15+
state,
16+
appName: "Calcom",
17+
};
18+
const query = stringify(params);
2119

22-
const appType = "huddle01_video";
23-
try {
24-
const alreadyInstalled = await prisma.credential.findFirst({
25-
where: {
26-
type: appType,
27-
...installForObject,
28-
},
29-
});
30-
if (alreadyInstalled) {
31-
throw new Error("Already installed");
32-
}
33-
const installation = await prisma.credential.create({
34-
data: {
35-
type: appType,
36-
key: {},
37-
...installForObject,
38-
appId: "huddle01",
39-
},
40-
});
41-
if (!installation) {
42-
throw new Error("Unable to create user credential for huddle01video");
43-
}
44-
} catch (error: unknown) {
45-
if (error instanceof Error) {
46-
return res.status(500).json({ message: error.message });
47-
}
48-
return res.status(500);
49-
}
50-
// need to return a json object with the response status
51-
return res
52-
.status(200)
53-
.json({ url: returnTo ?? getInstalledAppPath({ variant: "conferencing", slug: "huddle01" }) });
20+
const url = `https://huddle01.app/thirdparty_auth?${query}`;
21+
22+
return { url };
5423
}
24+
25+
export default defaultHandler({
26+
GET: Promise.resolve({ default: defaultResponder(handler) }),
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
4+
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
5+
6+
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
7+
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
8+
import { storeHuddle01Credential } from "../utils/storage";
9+
10+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
11+
const session = await getServerSession({ req, res });
12+
13+
const state = decodeOAuthState(req);
14+
15+
if (!session) {
16+
return res.status(401).json({ message: "Unauthorized" });
17+
}
18+
19+
const userId = session.user.id;
20+
21+
const { identityToken } = req.query;
22+
23+
const token = Array.isArray(identityToken) ? identityToken[0] : identityToken;
24+
if (!token) {
25+
return res.status(401).json({
26+
message: "Something went wrong",
27+
});
28+
}
29+
30+
await storeHuddle01Credential(userId, token);
31+
32+
res.redirect(
33+
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "conferencing", slug: "huddle01" })
34+
);
35+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default as add } from "./add";
2+
export { default as callback } from "./callback";
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,143 @@
1-
import z from "zod";
2-
3-
import { handleErrorsJson } from "@calcom/lib/errors";
4-
import { randomString } from "@calcom/lib/random";
1+
import logger from "@calcom/lib/logger";
2+
import type { CalendarEvent } from "@calcom/types/Calendar";
3+
import type { CredentialPayload } from "@calcom/types/Credential";
54
import type { PartialReference } from "@calcom/types/EventManager";
6-
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
5+
import type { VideoApiAdapter } from "@calcom/types/VideoApiAdapter";
6+
7+
import { getHuddle01APIKey, getHuddle01Credential } from "../utils/storage";
8+
9+
const API_END_POINT = "https://platform-api.huddle01.workers.dev/api/v2/calendar";
10+
11+
const fetchHuddleAPI = async (userId: number) => {
12+
const { identityToken } = await getHuddle01Credential(userId);
13+
const { apiKey } = await getHuddle01APIKey();
14+
15+
const headers = {
16+
"x-api-key": apiKey,
17+
"x-identity-token": identityToken,
18+
};
19+
return (
20+
endpoint: "subdomains" | "createMeeting" | "deleteMeeting" | "updateMeeting",
21+
option?: RequestInit
22+
) => {
23+
return fetch(`${API_END_POINT}/${endpoint}`, {
24+
...option,
25+
headers: {
26+
...option?.headers,
27+
...headers,
28+
},
29+
});
30+
};
31+
};
732

8-
const huddle01Schema = z.object({ url: z.string().url(), roomId: z.string() });
33+
const log = logger.getSubLogger({ prefix: ["app-store/huddle01video/lib/VideoApiAdapter"] });
934

10-
const Huddle01VideoApiAdapter = (): VideoApiAdapter => {
35+
const Huddle01ApiAdapter = (credential: CredentialPayload): VideoApiAdapter => {
1136
return {
12-
getAvailability: () => {
13-
return Promise.resolve([]);
14-
},
15-
createMeeting: async (): Promise<VideoCallData> => {
16-
const res = await fetch(
17-
"https://wpss2zlpb9.execute-api.us-east-1.amazonaws.com/new-meeting?utmCampaign=cal.com&utmSource=partner&utmMedium=calendar"
18-
);
19-
20-
const json = await handleErrorsJson<{ url: string }>(res);
21-
const { url } = huddle01Schema.parse(json);
22-
if (url) {
23-
return Promise.resolve({
37+
createMeeting: async (e: CalendarEvent) => {
38+
if (!credential.userId) {
39+
log.error("[Huddle01 Error] -> User is not logged in");
40+
throw new Error("User is not logged in");
41+
}
42+
43+
try {
44+
const fetch = await fetchHuddleAPI(credential.userId);
45+
46+
const res = await fetch("createMeeting", {
47+
method: "POST",
48+
body: JSON.stringify({
49+
title: e.title,
50+
startTime: e.startTime,
51+
endTime: e.endTime,
52+
}),
53+
headers: {
54+
"Content-Type": "application/json",
55+
},
56+
});
57+
58+
const data = (await res.json()) as {
59+
roomId: string;
60+
meetingLink: string;
61+
};
62+
63+
return {
2464
type: "huddle01_video",
25-
id: randomString(21),
65+
id: data.roomId,
2666
password: "",
27-
url,
28-
});
67+
url: data.meetingLink,
68+
};
69+
} catch (e) {
70+
log.error("[Huddle01 Error] -> Error while creating meeeting", e);
71+
throw Error("Error while creating meeting");
2972
}
30-
return Promise.reject("Url was not received in response body.");
3173
},
32-
deleteMeeting: async (): Promise<void> => {
33-
Promise.resolve();
34-
},
35-
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
36-
return Promise.resolve({
74+
updateMeeting: async (bookingRef: PartialReference, e: CalendarEvent) => {
75+
if (!credential.userId) {
76+
log.error("[Huddle01 Error] -> User is not logged in");
77+
throw new Error("User is not logged in");
78+
}
79+
80+
const fetch = await fetchHuddleAPI(credential.userId);
81+
82+
const res = await fetch("updateMeeting", {
83+
method: "PUT",
84+
body: JSON.stringify({
85+
title: e.title,
86+
startTime: e.startTime,
87+
endTime: e.endTime,
88+
meetingId: bookingRef.uid,
89+
}),
90+
headers: {
91+
"Content-Type": "application/json",
92+
},
93+
});
94+
95+
const data = (await res.json()) as {
96+
roomId: string;
97+
meetingLink: string;
98+
};
99+
100+
return {
37101
type: "huddle01_video",
38-
id: bookingRef.meetingId as string,
39-
password: bookingRef.meetingPassword as string,
40-
url: bookingRef.meetingUrl as string,
102+
id: data.roomId,
103+
password: "",
104+
url: data.meetingLink,
105+
};
106+
},
107+
deleteMeeting: async (meetingId: string) => {
108+
if (!credential.userId) {
109+
log.error("[Huddle01 Error] -> User is not logged in");
110+
throw new Error("User is not logged in");
111+
}
112+
113+
const fetch = await fetchHuddleAPI(credential.userId);
114+
115+
const res = await fetch("deleteMeeting", {
116+
method: "DELETE",
117+
body: JSON.stringify({
118+
meetingId,
119+
}),
120+
headers: {
121+
"Content-Type": "application/json",
122+
},
41123
});
124+
125+
const data = (await res.json()) as {
126+
roomId: string;
127+
meetingLink: string;
128+
};
129+
130+
return {
131+
type: "huddle01_video",
132+
id: data.roomId,
133+
password: "",
134+
url: data.meetingLink,
135+
};
136+
},
137+
getAvailability: async () => {
138+
return [];
42139
},
43140
};
44141
};
45142

46-
export default Huddle01VideoApiAdapter;
143+
export default Huddle01ApiAdapter;

0 commit comments

Comments
 (0)