diff --git a/packages/app-store/_utils/useAddAppMutation.ts b/packages/app-store/_utils/useAddAppMutation.ts index 9a2cfa5612..6530e9516e 100644 --- a/packages/app-store/_utils/useAddAppMutation.ts +++ b/packages/app-store/_utils/useAddAppMutation.ts @@ -1,5 +1,6 @@ import type { UseMutationOptions } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query"; +import { usePathname } from "next/navigation"; import type { IntegrationOAuthCallbackState } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -26,8 +27,20 @@ type UseAddAppMutationOptions = CustomUseMutationOptions & { returnTo?: string; }; +type useAddAppMutationVariables = { + type?: App["type"]; + variant?: string; + slug?: string; + isOmniInstall?: boolean; + teamId?: number; + onErrorReturnTo?: string; +}; + function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMutationOptions) { const { returnTo, ...options } = allOptions || {}; + const pathname = usePathname(); + const onErrorReturnTo = `${WEBAPP_URL}${pathname}`; + const mutation = useMutation< AddAppMutationData, Error, @@ -59,6 +72,8 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta { variant: variables && variables.variant, slug: variables && variables.slug }, location.search ), + onErrorReturnTo, + fromApp: true, ...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }), ...(teamId && { teamId }), }; diff --git a/packages/app-store/googlecalendar/api/add.ts b/packages/app-store/googlecalendar/api/add.ts index 7ed6fcf02d..6d2c65385a 100644 --- a/packages/app-store/googlecalendar/api/add.ts +++ b/packages/app-store/googlecalendar/api/add.ts @@ -8,7 +8,7 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState"; -const scopes = [ +export const scopes = [ "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events", ]; diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index 3577e9b092..3503d6cce7 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -1,7 +1,7 @@ import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; -import { WEBAPP_URL_FOR_OAUTH, CAL_URL } from "@calcom/lib/constants"; +import { WEBAPP_URL_FOR_OAUTH, WEBAPP_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { HttpError } from "@calcom/lib/http-error"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; @@ -10,6 +10,7 @@ import prisma from "@calcom/prisma"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; +import { scopes } from "./add"; let client_id = ""; let client_secret = ""; @@ -19,6 +20,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const state = decodeOAuthState(req); if (typeof code !== "string") { + if (state?.onErrorReturnTo || state?.returnTo) { + res.redirect( + getSafeRedirectUrl(state.onErrorReturnTo) ?? + getSafeRedirectUrl(state?.returnTo) ?? + `${WEBAPP_URL}/apps/installed` + ); + return; + } throw new HttpError({ statusCode: 400, message: "`code` must be a string" }); } @@ -36,12 +45,32 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); - let key = ""; + let key; if (code) { const token = await oAuth2Client.getToken(code); key = token.res?.data; + // Check that the has granted all permissions + const grantedScopes = key.scope; + for (const scope of scopes) { + if (!grantedScopes.includes(scope)) { + if (!state?.fromApp) { + throw new HttpError({ + statusCode: 400, + message: "You must grant all permissions to use this integration", + }); + } else { + res.redirect( + getSafeRedirectUrl(state.onErrorReturnTo) ?? + getSafeRedirectUrl(state?.returnTo) ?? + `${WEBAPP_URL}/apps/installed` + ); + return; + } + } + } + const credential = await prisma.credential.create({ data: { type: "google_calendar", @@ -98,7 +127,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { }); res.redirect( - getSafeRedirectUrl(`${CAL_URL}/apps/installed/conferencing?hl=google-meet`) ?? + getSafeRedirectUrl(`${WEBAPP_URL}/apps/installed/conferencing?hl=google-meet`) ?? getInstalledAppPath({ variant: "conferencing", slug: "google-meet" }) ); } diff --git a/packages/app-store/office365calendar/api/callback.ts b/packages/app-store/office365calendar/api/callback.ts index 4de204b52c..afe1bf83f9 100644 --- a/packages/app-store/office365calendar/api/callback.ts +++ b/packages/app-store/office365calendar/api/callback.ts @@ -17,8 +17,17 @@ let client_secret = ""; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { code } = req.query; + const state = decodeOAuthState(req); if (typeof code !== "string") { + if (state?.onErrorReturnTo || state?.returnTo) { + res.redirect( + getSafeRedirectUrl(state.onErrorReturnTo) ?? + getSafeRedirectUrl(state?.returnTo) ?? + `${WEBAPP_URL}/apps/installed` + ); + return; + } res.status(400).json({ message: "No code returned" }); return; } @@ -120,7 +129,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } - const state = decodeOAuthState(req); return res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "calendar", slug: "office365-calendar" }) diff --git a/packages/app-store/office365video/api/callback.ts b/packages/app-store/office365video/api/callback.ts index aee22938cf..fd2db4ae66 100644 --- a/packages/app-store/office365video/api/callback.ts +++ b/packages/app-store/office365video/api/callback.ts @@ -16,8 +16,17 @@ let client_secret = ""; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { code } = req.query; + const state = decodeOAuthState(req); if (typeof code !== "string") { + if (state?.onErrorReturnTo || state?.returnTo) { + res.redirect( + getSafeRedirectUrl(state.onErrorReturnTo) ?? + getSafeRedirectUrl(state?.returnTo) ?? + `${WEBAPP_URL}/apps/installed` + ); + return; + } res.status(400).json({ message: "No code returned" }); return; } @@ -95,7 +104,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req); - const state = decodeOAuthState(req); return res.redirect( getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "conferencing", slug: "msteams" }) ); diff --git a/packages/app-store/stripepayment/api/callback.ts b/packages/app-store/stripepayment/api/callback.ts index dad6067690..98e3b4ca87 100644 --- a/packages/app-store/stripepayment/api/callback.ts +++ b/packages/app-store/stripepayment/api/callback.ts @@ -4,6 +4,7 @@ import { stringify } from "querystring"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential"; +import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; import type { StripeData } from "../lib/server"; import stripe from "../lib/server"; @@ -19,8 +20,13 @@ function getReturnToValueFromQueryState(req: NextApiRequest) { export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { code, error, error_description } = req.query; + const state = decodeOAuthState(req); if (error) { + // User cancels flow + if (error === "access_denied") { + state?.onErrorReturnTo ? res.redirect(state.onErrorReturnTo) : res.redirect("/apps/installed/payment"); + } const query = stringify({ error, error_description }); res.redirect(`/apps/installed?${query}`); return; diff --git a/packages/app-store/types.d.ts b/packages/app-store/types.d.ts index d642562fff..dfb22ab85b 100644 --- a/packages/app-store/types.d.ts +++ b/packages/app-store/types.d.ts @@ -7,6 +7,8 @@ import type { ButtonProps } from "@calcom/ui"; export type IntegrationOAuthCallbackState = { returnTo: string; + onErrorReturnTo: string; + fromApp: boolean; installGoogleVideo?: boolean; teamId?: number; }; diff --git a/packages/features/calendars/weeklyview/components/Calendar.tsx b/packages/features/calendars/weeklyview/components/Calendar.tsx index ae5fa62ffd..533fb31145 100644 --- a/packages/features/calendars/weeklyview/components/Calendar.tsx +++ b/packages/features/calendars/weeklyview/components/Calendar.tsx @@ -66,7 +66,7 @@ export function Calendar(props: CalendarComponentProps) { style={{ width: "165%" }} className="flex h-full max-w-full flex-none flex-col sm:max-w-none md:max-w-full"> -
+