From 8bc265faaa083905f2ff46dfa25422f613a363fc Mon Sep 17 00:00:00 2001
From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Date: Mon, 29 Jan 2024 03:51:53 -0500
Subject: [PATCH] fix: When declining OAuth return to previous page (#13189)
* Get `onErrorReturnTo` URL
* If no code return to previous page
* On invalid permissions, return to previous page
* When not enough permissions given, invalidate the credential created
* Type fix
* Stripe go back to previous page on cancel
* Clean up console.logs
* Add early returns when redirecting
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
---
.../app-store/_utils/useAddAppMutation.ts | 15 ++++++++
packages/app-store/googlecalendar/api/add.ts | 2 +-
.../app-store/googlecalendar/api/callback.ts | 35 +++++++++++++++++--
.../office365calendar/api/callback.ts | 10 +++++-
.../app-store/office365video/api/callback.ts | 10 +++++-
.../app-store/stripepayment/api/callback.ts | 6 ++++
packages/app-store/types.d.ts | 2 ++
.../weeklyview/components/Calendar.tsx | 2 +-
8 files changed, 75 insertions(+), 7 deletions(-)
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">