diff --git a/components/integrations/ConnectIntegrations.tsx b/components/integrations/ConnectIntegrations.tsx index 3e96f7e7..8534963c 100644 --- a/components/integrations/ConnectIntegrations.tsx +++ b/components/integrations/ConnectIntegrations.tsx @@ -1,3 +1,4 @@ +import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types"; import { useState } from "react"; import { useMutation } from "react-query"; @@ -13,8 +14,14 @@ export default function ConnectIntegration(props: { }) { const { type } = props; const [isLoading, setIsLoading] = useState(false); + const mutation = useMutation(async () => { - const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add"); + const state: IntegrationOAuthCallbackState = { + returnTo: location.pathname + location.search, + }; + const stateStr = encodeURIComponent(JSON.stringify(state)); + const searchParams = `?state=${stateStr}`; + const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add" + searchParams); if (!res.ok) { throw new Error("Something went wrong"); } diff --git a/pages/api/integrations/googlecalendar/add.ts b/pages/api/integrations/googlecalendar/add.ts index f7a57688..30ada67e 100644 --- a/pages/api/integrations/googlecalendar/add.ts +++ b/pages/api/integrations/googlecalendar/add.ts @@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; +import { encodeOAuthState } from "../utils"; + const credentials = process.env.GOOGLE_API_CREDENTIALS!; const scopes = [ "https://www.googleapis.com/auth/calendar.readonly", @@ -32,6 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // setting the prompt to 'consent' will force this consent // every time, forcing a refresh_token to be returned. prompt: "consent", + state: encodeOAuthState(req), }); res.status(200).json({ url: authUrl }); diff --git a/pages/api/integrations/googlecalendar/callback.ts b/pages/api/integrations/googlecalendar/callback.ts index 6b6974cb..778ae47a 100644 --- a/pages/api/integrations/googlecalendar/callback.ts +++ b/pages/api/integrations/googlecalendar/callback.ts @@ -4,6 +4,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; import prisma from "@lib/prisma"; +import { decodeOAuthState } from "../utils"; + const credentials = process.env.GOOGLE_API_CREDENTIALS; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -29,6 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback"; const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const token = await oAuth2Client.getToken(code); + const key = token.res?.data; await prisma.credential.create({ data: { @@ -37,6 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) userId: session.user.id, }, }); - - res.redirect("/integrations"); + const state = decodeOAuthState(req); + res.redirect(state?.returnTo ?? "/integrations"); } diff --git a/pages/api/integrations/office365calendar/add.ts b/pages/api/integrations/office365calendar/add.ts index a3ae606f..e0d7501f 100644 --- a/pages/api/integrations/office365calendar/add.ts +++ b/pages/api/integrations/office365calendar/add.ts @@ -2,42 +2,32 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; -import prisma from "../../../../lib/prisma"; +import { encodeOAuthState } from "../utils"; const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"]; -function generateAuthUrl() { - return ( - "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" + - scopes.join(" ") + - "&client_id=" + - process.env.MS_GRAPH_CLIENT_ID + - "&redirect_uri=" + - process.env.BASE_URL + - "/api/integrations/office365calendar/callback" - ); -} - export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "GET") { // Check that user is authenticated const session = await getSession({ req: req }); - if (!session) { + if (!session?.user) { res.status(401).json({ message: "You must be logged in to do this" }); return; } - // Get user - await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - }, - }); - - res.status(200).json({ url: generateAuthUrl() }); + const state = encodeOAuthState(req); + let url = + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" + + scopes.join(" ") + + "&client_id=" + + process.env.MS_GRAPH_CLIENT_ID + + "&redirect_uri=" + + process.env.BASE_URL + + "/api/integrations/office365calendar/callback"; + if (state) { + url += "&state=" + encodeURIComponent(state); + } + res.status(200).json({ url }); } } diff --git a/pages/api/integrations/office365calendar/callback.ts b/pages/api/integrations/office365calendar/callback.ts index ecc4c8a4..d0cdb2ef 100644 --- a/pages/api/integrations/office365calendar/callback.ts +++ b/pages/api/integrations/office365calendar/callback.ts @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; import prisma from "../../../../lib/prisma"; +import { decodeOAuthState } from "../utils"; const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"]; @@ -11,23 +12,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Check that user is authenticated const session = await getSession({ req: req }); - if (!session) { + if (!session?.user?.id) { res.status(401).json({ message: "You must be logged in to do this" }); return; } + if (typeof code !== "string") { + res.status(400).json({ message: "No code returned" }); + return; + } - const toUrlEncoded = (payload) => + const toUrlEncoded = (payload: Record) => Object.keys(payload) .map((key) => key + "=" + encodeURIComponent(payload[key])) .join("&"); const body = toUrlEncoded({ - client_id: process.env.MS_GRAPH_CLIENT_ID, + client_id: process.env.MS_GRAPH_CLIENT_ID!, grant_type: "authorization_code", code, scope: scopes.join(" "), redirect_uri: process.env.BASE_URL + "/api/integrations/office365calendar/callback", - client_secret: process.env.MS_GRAPH_CLIENT_SECRET, + client_secret: process.env.MS_GRAPH_CLIENT_SECRET!, }); const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { @@ -62,5 +67,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - return res.redirect("/integrations"); + const state = decodeOAuthState(req); + return res.redirect(state?.returnTo ?? "/integrations"); } diff --git a/pages/api/integrations/types.d.ts b/pages/api/integrations/types.d.ts new file mode 100644 index 00000000..bcdc5dde --- /dev/null +++ b/pages/api/integrations/types.d.ts @@ -0,0 +1,3 @@ +export type IntegrationOAuthCallbackState = { + returnTo: string; +}; diff --git a/pages/api/integrations/utils.ts b/pages/api/integrations/utils.ts new file mode 100644 index 00000000..8353ac43 --- /dev/null +++ b/pages/api/integrations/utils.ts @@ -0,0 +1,21 @@ +import { NextApiRequest } from "next"; + +import { IntegrationOAuthCallbackState } from "./types"; + +export function encodeOAuthState(req: NextApiRequest) { + if (typeof req.query.state !== "string") { + return undefined; + } + const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state); + + return JSON.stringify(state); +} + +export function decodeOAuthState(req: NextApiRequest) { + if (typeof req.query.state !== "string") { + return undefined; + } + const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state); + + return state; +}