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>
This commit is contained in:
co-authored by
sean-brydon
Udit Takkar
parent
f33ea91ab0
commit
8bc265faaa
@@ -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 }),
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
@@ -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" })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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" })
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+2
@@ -7,6 +7,8 @@ import type { ButtonProps } from "@calcom/ui";
|
||||
|
||||
export type IntegrationOAuthCallbackState = {
|
||||
returnTo: string;
|
||||
onErrorReturnTo: string;
|
||||
fromApp: boolean;
|
||||
installGoogleVideo?: boolean;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
<DateValues containerNavRef={containerNav} days={days} />
|
||||
<div className="relative flex flex-auto h-screen overflow-y-auto">
|
||||
<div className="relative flex h-screen flex-auto overflow-y-auto">
|
||||
<CurrentTime />
|
||||
<div className="bg-default dark:bg-muted ring-muted border-default sticky left-0 z-10 w-14 flex-none border-l border-r ring-1" />
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user