diff --git a/apps/web/pages/api/integrations/[...args].ts b/apps/web/pages/api/integrations/[...args].ts index b0cb1f7b86..ea1b68dbf5 100644 --- a/apps/web/pages/api/integrations/[...args].ts +++ b/apps/web/pages/api/integrations/[...args].ts @@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import type { Session } from "next-auth"; import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath"; +import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType"; import { HttpError } from "@calcom/lib/http-error"; @@ -37,6 +38,9 @@ const defaultIntegrationAddHandler = async ({ throw new Error("App is already installed"); } } + + await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId: user.id }); + await createCredential({ user: user, appType, slug, teamId }); }; @@ -72,7 +76,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(200); } catch (error) { console.error(error); - if (error instanceof HttpError) { return res.status(error.statusCode).json({ message: error.message }); } diff --git a/apps/web/pages/api/teams/googleworkspace/callback.ts b/apps/web/pages/api/teams/googleworkspace/callback.ts index 079353b4af..07b17cfaa8 100644 --- a/apps/web/pages/api/teams/googleworkspace/callback.ts +++ b/apps/web/pages/api/teams/googleworkspace/callback.ts @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; +import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; @@ -22,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { code, state } = req.query; const parsedState = stateSchema.parse(JSON.parse(state as string)); const { teamId } = parsedState; - + await throwIfNotHaveAdminAccessToTeam({ teamId: Number(teamId) ?? null, userId: session.user.id }); if (code && typeof code !== "string") { res.status(400).json({ message: "`code` must be a string" }); return; diff --git a/packages/app-store/_utils/createOAuthAppCredential.ts b/packages/app-store/_utils/createOAuthAppCredential.ts index f647852b4e..fdd3dbe025 100644 --- a/packages/app-store/_utils/createOAuthAppCredential.ts +++ b/packages/app-store/_utils/createOAuthAppCredential.ts @@ -1,8 +1,10 @@ import type { NextApiRequest } from "next"; +import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; import { decodeOAuthState } from "./decodeOAuthState"; +import { throwIfNotHaveAdminAccessToTeam } from "./throwIfNotHaveAdminAccessToTeam"; /** * This function is used to create app credentials for either a user or a team @@ -19,8 +21,12 @@ const createOAuthAppCredential = async ( req: NextApiRequest ) => { const userId = req.session?.user.id; + if (!userId) { + throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" }); + } // For OAuth flows, see if a teamId was passed through the state const state = decodeOAuthState(req); + if (state?.teamId) { // Check that the user belongs to the team const team = await prisma.team.findFirst({ @@ -50,6 +56,8 @@ const createOAuthAppCredential = async ( return; } + await throwIfNotHaveAdminAccessToTeam({ teamId: state?.teamId ?? null, userId }); + await prisma.credential.create({ data: { type: appData.type, diff --git a/packages/app-store/_utils/throwIfNotHaveAdminAccessToTeam.ts b/packages/app-store/_utils/throwIfNotHaveAdminAccessToTeam.ts new file mode 100644 index 0000000000..63b9cf9105 --- /dev/null +++ b/packages/app-store/_utils/throwIfNotHaveAdminAccessToTeam.ts @@ -0,0 +1,20 @@ +import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams"; +import { HttpError } from "@calcom/lib/http-error"; + +export const throwIfNotHaveAdminAccessToTeam = async ({ + teamId, + userId, +}: { + teamId: number | null; + userId: number; +}) => { + if (!teamId) { + return; + } + const teamsUserHasAdminAccessFor = await getUserAdminTeams({ userId }); + const hasAdminAccessToTeam = teamsUserHasAdminAccessFor.some((team) => team.id === teamId); + + if (!hasAdminAccessToTeam) { + throw new HttpError({ statusCode: 401, message: "You must be an admin of the team to do this" }); + } +}; diff --git a/packages/app-store/giphy/api/add.ts b/packages/app-store/giphy/api/add.ts index 01a9b4af8c..ae3e40ee66 100644 --- a/packages/app-store/giphy/api/add.ts +++ b/packages/app-store/giphy/api/add.ts @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "@calcom/prisma"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import { throwIfNotHaveAdminAccessToTeam } from "../../_utils/throwIfNotHaveAdminAccessToTeam"; /** * This is an example endpoint for an app, these will run under `/api/integrations/[...args]` @@ -13,10 +14,14 @@ 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 userId = req.session.user.id; const appType = "giphy_other"; - const credentialOwner = req.query.teamId - ? { teamId: Number(req.query.teamId) } - : { userId: req.session.user.id }; + const teamId = Number(req.query.teamId); + const credentialOwner = req.query.teamId ? { teamId } : { userId: req.session.user.id }; + + await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId }); + try { const alreadyInstalled = await prisma.credential.findFirst({ where: { diff --git a/packages/app-store/hubspot/api/callback.ts b/packages/app-store/hubspot/api/callback.ts index 8d31b7922c..f4973b2ff5 100644 --- a/packages/app-store/hubspot/api/callback.ts +++ b/packages/app-store/hubspot/api/callback.ts @@ -47,7 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // set expiry date as offset from current time. hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000); - createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken as any, req); + await createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken as any, req); const state = decodeOAuthState(req); res.redirect( diff --git a/packages/app-store/office365video/api/callback.ts b/packages/app-store/office365video/api/callback.ts index ff69b8a85b..2a9c8bafe4 100644 --- a/packages/app-store/office365video/api/callback.ts +++ b/packages/app-store/office365video/api/callback.ts @@ -93,7 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } }); } - createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req); + await createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req); const state = decodeOAuthState(req); return res.redirect( diff --git a/packages/app-store/salesforce/api/callback.ts b/packages/app-store/salesforce/api/callback.ts index f7d885309f..647663572a 100644 --- a/packages/app-store/salesforce/api/callback.ts +++ b/packages/app-store/salesforce/api/callback.ts @@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const salesforceTokenInfo = await conn.oauth2.requestToken(code as string); - createOAuthAppCredential( + await createOAuthAppCredential( { appId: "salesforce", type: "salesforce_other_calendar" }, salesforceTokenInfo as any, req diff --git a/packages/app-store/stripepayment/api/callback.ts b/packages/app-store/stripepayment/api/callback.ts index cbcc593129..fcb94a8c3f 100644 --- a/packages/app-store/stripepayment/api/callback.ts +++ b/packages/app-store/stripepayment/api/callback.ts @@ -44,7 +44,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) data["default_currency"] = account.default_currency; } - createOAuthAppCredential( + await createOAuthAppCredential( { appId: "stripe", type: "stripe_payment" }, data as unknown as Prisma.InputJsonObject, req diff --git a/packages/app-store/tandemvideo/api/callback.ts b/packages/app-store/tandemvideo/api/callback.ts index f213a328ae..f5275f4f74 100644 --- a/packages/app-store/tandemvideo/api/callback.ts +++ b/packages/app-store/tandemvideo/api/callback.ts @@ -60,7 +60,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000); delete responseBody.expires_in; - createOAuthAppCredential({ appId: "tandem", type: "tandem_video" }, responseBody, req); + await createOAuthAppCredential({ appId: "tandem", type: "tandem_video" }, responseBody, req); res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "tandem" })); } diff --git a/packages/app-store/webex/api/callback.ts b/packages/app-store/webex/api/callback.ts index c6c6a8ad98..b2f5bad081 100644 --- a/packages/app-store/webex/api/callback.ts +++ b/packages/app-store/webex/api/callback.ts @@ -81,7 +81,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } }); } - createOAuthAppCredential({ appId: config.slug, type: config.type }, responseBody, req); + await createOAuthAppCredential({ appId: config.slug, type: config.type }, responseBody, req); res.redirect(getInstalledAppPath({ variant: config.variant, slug: config.slug })); } diff --git a/packages/app-store/zoho-bigin/api/callback.ts b/packages/app-store/zoho-bigin/api/callback.ts index aa11e74125..c8f219045f 100644 --- a/packages/app-store/zoho-bigin/api/callback.ts +++ b/packages/app-store/zoho-bigin/api/callback.ts @@ -52,7 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in); tokenInfo.data.accountServer = accountsServer; - createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, tokenInfo.data, req); + await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, tokenInfo.data, req); const state = decodeOAuthState(req); res.redirect( diff --git a/packages/app-store/zohocrm/api/callback.ts b/packages/app-store/zohocrm/api/callback.ts index f161beb8b2..c818d099d7 100644 --- a/packages/app-store/zohocrm/api/callback.ts +++ b/packages/app-store/zohocrm/api/callback.ts @@ -51,7 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60); zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"]; - createOAuthAppCredential( + await createOAuthAppCredential( { appId: "zohocrm", type: "zohocrm_other_calendar" }, zohoCrmTokenInfo.data as any, req diff --git a/packages/app-store/zoomvideo/api/callback.ts b/packages/app-store/zoomvideo/api/callback.ts index 461d7fd46e..b97bab0f02 100644 --- a/packages/app-store/zoomvideo/api/callback.ts +++ b/packages/app-store/zoomvideo/api/callback.ts @@ -70,7 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } }); } - createOAuthAppCredential({ appId: "zoom", type: "zoom_video" }, responseBody, req); + await createOAuthAppCredential({ appId: "zoom", type: "zoom_video" }, responseBody, req); res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "zoom" })); }