Fix onboarding OAuth callback glitch (#1079)
This commit is contained in:
parent
d147772d91
commit
a002b194da
7 changed files with 66 additions and 33 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
|
|
||||||
|
@ -13,8 +14,14 @@ export default function ConnectIntegration(props: {
|
||||||
}) {
|
}) {
|
||||||
const { type } = props;
|
const { type } = props;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const mutation = useMutation(async () => {
|
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) {
|
if (!res.ok) {
|
||||||
throw new Error("Something went wrong");
|
throw new Error("Something went wrong");
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
|
|
||||||
|
import { encodeOAuthState } from "../utils";
|
||||||
|
|
||||||
const credentials = process.env.GOOGLE_API_CREDENTIALS!;
|
const credentials = process.env.GOOGLE_API_CREDENTIALS!;
|
||||||
const scopes = [
|
const scopes = [
|
||||||
"https://www.googleapis.com/auth/calendar.readonly",
|
"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
|
// setting the prompt to 'consent' will force this consent
|
||||||
// every time, forcing a refresh_token to be returned.
|
// every time, forcing a refresh_token to be returned.
|
||||||
prompt: "consent",
|
prompt: "consent",
|
||||||
|
state: encodeOAuthState(req),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({ url: authUrl });
|
res.status(200).json({ url: authUrl });
|
||||||
|
|
|
@ -4,6 +4,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
import { decodeOAuthState } from "../utils";
|
||||||
|
|
||||||
const credentials = process.env.GOOGLE_API_CREDENTIALS;
|
const credentials = process.env.GOOGLE_API_CREDENTIALS;
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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 redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback";
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
const token = await oAuth2Client.getToken(code);
|
const token = await oAuth2Client.getToken(code);
|
||||||
|
|
||||||
const key = token.res?.data;
|
const key = token.res?.data;
|
||||||
await prisma.credential.create({
|
await prisma.credential.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -37,6 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const state = decodeOAuthState(req);
|
||||||
res.redirect("/integrations");
|
res.redirect(state?.returnTo ?? "/integrations");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,42 +2,32 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
|
|
||||||
import prisma from "../../../../lib/prisma";
|
import { encodeOAuthState } from "../utils";
|
||||||
|
|
||||||
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
|
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) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
// Check that user is authenticated
|
// Check that user is authenticated
|
||||||
const session = await getSession({ req: req });
|
const session = await getSession({ req: req });
|
||||||
|
|
||||||
if (!session) {
|
if (!session?.user) {
|
||||||
res.status(401).json({ message: "You must be logged in to do this" });
|
res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user
|
const state = encodeOAuthState(req);
|
||||||
await prisma.user.findFirst({
|
let url =
|
||||||
where: {
|
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" +
|
||||||
email: session.user.email,
|
scopes.join(" ") +
|
||||||
},
|
"&client_id=" +
|
||||||
select: {
|
process.env.MS_GRAPH_CLIENT_ID +
|
||||||
id: true,
|
"&redirect_uri=" +
|
||||||
},
|
process.env.BASE_URL +
|
||||||
});
|
"/api/integrations/office365calendar/callback";
|
||||||
|
if (state) {
|
||||||
res.status(200).json({ url: generateAuthUrl() });
|
url += "&state=" + encodeURIComponent(state);
|
||||||
|
}
|
||||||
|
res.status(200).json({ url });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
|
|
||||||
import prisma from "../../../../lib/prisma";
|
import prisma from "../../../../lib/prisma";
|
||||||
|
import { decodeOAuthState } from "../utils";
|
||||||
|
|
||||||
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
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
|
// Check that user is authenticated
|
||||||
const session = await getSession({ req: req });
|
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" });
|
res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (typeof code !== "string") {
|
||||||
|
res.status(400).json({ message: "No code returned" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const toUrlEncoded = (payload) =>
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
Object.keys(payload)
|
Object.keys(payload)
|
||||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
const body = toUrlEncoded({
|
const body = toUrlEncoded({
|
||||||
client_id: process.env.MS_GRAPH_CLIENT_ID,
|
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code,
|
code,
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
redirect_uri: process.env.BASE_URL + "/api/integrations/office365calendar/callback",
|
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", {
|
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");
|
||||||
}
|
}
|
||||||
|
|
3
pages/api/integrations/types.d.ts
vendored
Normal file
3
pages/api/integrations/types.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export type IntegrationOAuthCallbackState = {
|
||||||
|
returnTo: string;
|
||||||
|
};
|
21
pages/api/integrations/utils.ts
Normal file
21
pages/api/integrations/utils.ts
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue