-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Slack/reform #1852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Slack/reform #1852
Changes from all commits
2ec19e7
6170031
3e121a0
b70dabd
1398e65
f4e19c1
58d2459
4026635
beea381
de4cc50
0cb076f
9a90173
cdce68c
fcda408
3fa8929
ff9a550
e106ba4
aa4c443
977fee1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||||||||||||||||||||||||
| import { NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import { authOptions } from "@/pages/api/auth/[...nextauth]"; | ||||||||||||||||||||||||||||||||||||
| import { getServerSession } from "next-auth/next"; | ||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import { getSlackInstallationUrl } from "@/lib/integrations/slack/install"; | ||||||||||||||||||||||||||||||||||||
| import prisma from "@/lib/prisma"; | ||||||||||||||||||||||||||||||||||||
| import { CustomUser } from "@/lib/types"; | ||||||||||||||||||||||||||||||||||||
| import { getSearchParams } from "@/lib/utils/get-search-params"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const oAuthAuthorizeSchema = z.object({ | ||||||||||||||||||||||||||||||||||||
| teamId: z.string().cuid(), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export async function GET(req: Request) { | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const session = await getServerSession(authOptions); | ||||||||||||||||||||||||||||||||||||
| if (!session) { | ||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const { teamId } = oAuthAuthorizeSchema.parse(getSearchParams(req.url)); | ||||||||||||||||||||||||||||||||||||
| console.log("teamId", teamId); | ||||||||||||||||||||||||||||||||||||
| const userId = (session.user as CustomUser).id; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const userTeam = await prisma.userTeam.findUnique({ | ||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||
| userId_teamId: { | ||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||
| teamId, | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!userTeam) { | ||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: "Access denied" }, { status: 403 }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const oauthUrl = await getSlackInstallationUrl(teamId); | ||||||||||||||||||||||||||||||||||||
| console.log("oauthUrl", oauthUrl); | ||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not log the OAuth URL (leaks state). oauthUrl includes the state param. Avoid logging it. - console.log("oauthUrl", oauthUrl);
+ // Avoid logging oauthUrl; it contains sensitive `state`.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||
| oauthUrl, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||
| console.error("Slack OAuth authorization error:", error); | ||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||
| { error: "Internal server error" }, | ||||||||||||||||||||||||||||||||||||
| { status: 500 }, | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Return 400 on validation errors instead of 500. Handle Zod parsing failures explicitly. - } catch (error) {
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json({ error: "Invalid request" }, { status: 400 });
+ }
console.error("Slack OAuth authorization error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { redirect } from "next/navigation"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Team } from "@prisma/client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSession } from "next-auth/react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import z from "zod"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+6
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use getServerSession(authOptions) in App Router route. getSession from next-auth/react is for client; it will not work reliably in a server route. -import { getSession } from "next-auth/react";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";- const session = await getSession();
+ const session = await getServerSession(authOptions);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { installIntegration } from "@/lib/integrations/install"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSlackEnv } from "@/lib/integrations/slack/env"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SlackCredential } from "@/lib/integrations/slack/types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { encryptSlackToken } from "@/lib/integrations/slack/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import prisma from "@/lib/prisma"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { redis } from "@/lib/redis"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CustomUser } from "@/lib/types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSearchParams } from "@/lib/utils/get-search-params"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const dynamic = "force-dynamic"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const oAuthCallbackSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const GET = async (req: Request) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const env = getSlackEnv(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let workspace: Pick<Team, "id" | "plan"> | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const session = await getSession(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!session) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Unauthorized"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userId = (session.user as CustomUser).id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { code, state } = oAuthCallbackSchema.parse(getSearchParams(req.url)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find workspace that initiated the Stripe app install | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const teamId = await redis.get<string>(`slack:install:state:${state}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!teamId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Unknown state"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspace = await prisma.team.findUniqueOrThrow({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: teamId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| select: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| plan: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append("code", code); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append("client_id", env.SLACK_CLIENT_ID); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append("client_secret", env.SLACK_CLIENT_SECRET); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "redirect_uri", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${process.env.NEXT_PUBLIC_BASE_URL}/api/integrations/slack/oauth/callback`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch("https://slack.com/api/oauth.v2.access", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: formData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("data", data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const credentials: SlackCredential = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| appId: data.app_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| botUserId: data.bot_user_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scope: data.scope, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accessToken: encryptSlackToken(data.access_token), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokenType: data.token_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authUser: data.authed_user, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| team: data.team, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Validate Slack response and avoid logging secrets. Check data.ok, surface Slack errors, and stop logging the full payload which contains tokens. - const data = await response.json();
-
- console.log("data", data);
-
- const credentials: SlackCredential = {
- appId: data.app_id,
- botUserId: data.bot_user_id,
- scope: data.scope,
- accessToken: encryptSlackToken(data.access_token),
- tokenType: data.token_type,
- authUser: data.authed_user,
- team: data.team,
- };
+ const data = await response.json();
+ if (!data?.ok) {
+ throw new Error(`Slack OAuth failed: ${data?.error ?? "unknown_error"}`);
+ }
+ const {
+ app_id,
+ bot_user_id,
+ scope,
+ access_token,
+ token_type,
+ authed_user,
+ team,
+ } = data;
+ const credentials: SlackCredential = {
+ appId: app_id,
+ botUserId: bot_user_id,
+ scope,
+ accessToken: encryptSlackToken(access_token),
+ tokenType: token_type,
+ authUser: authed_user,
+ team,
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await installIntegration({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| integrationId: env.SLACK_INTEGRATION_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| teamId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| credentials, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: any) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+84
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Delete the Redis state after a successful install. Prevents replay and keeps Redis tidy. await installIntegration({
integrationId: env.SLACK_INTEGRATION_ID,
userId,
teamId,
credentials,
});
+ await redis.del(`slack:install:state:${state}`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: e.message }, { status: 500 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirect(`/settings/slack`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import React from "react"; | ||
|
|
||
| import { | ||
| Body, | ||
| Button, | ||
| Container, | ||
| Head, | ||
| Hr, | ||
| Html, | ||
| Preview, | ||
| Section, | ||
| Tailwind, | ||
| Text, | ||
| } from "@react-email/components"; | ||
|
|
||
| import { Footer } from "./shared/footer"; | ||
|
|
||
| export default function SlackIntegrationNotification({ | ||
| email = "[email protected]", | ||
| team = { | ||
| name: "Acme, Inc", | ||
| }, | ||
| integration = { | ||
| name: "Slack", | ||
| slug: "slack", | ||
| }, | ||
| }: { | ||
| email: string; | ||
| team: { | ||
| name: string; | ||
| }; | ||
| integration: { | ||
| name: string; | ||
| slug: string; | ||
| }; | ||
| }) { | ||
| return ( | ||
| <Html> | ||
| <Head /> | ||
| <Preview>An integration has been added to your team</Preview> | ||
| <Tailwind> | ||
| <Body className="mx-auto my-auto bg-white font-sans"> | ||
| <Container className="mx-auto my-10 w-[465px] p-5"> | ||
| <Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal"> | ||
| <span className="font-bold tracking-tighter">Papermark</span> | ||
| </Text> | ||
| <Text className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black"> | ||
| An integration has been added to your team | ||
| </Text> | ||
| <Text className="text-sm leading-6 text-black"> | ||
| The <strong>{integration.name}</strong> integration has been added | ||
| to your team {team.name} on Papermark. | ||
| </Text> | ||
| <Text className="text-sm leading-6 text-black"> | ||
| You can now receive notifications about document views, dataroom | ||
| access and downloads directly in your Slack channels. | ||
| </Text> | ||
| <Section className="my-8 text-center"> | ||
| <Button | ||
| className="rounded bg-black text-center text-xs font-semibold text-white no-underline" | ||
| href={`${process.env.NEXT_PUBLIC_BASE_URL}/settings/integrations/${integration.slug}`} | ||
| style={{ padding: "12px 20px" }} | ||
| > | ||
| View installed integration | ||
| </Button> | ||
| </Section> | ||
|
|
||
| <Footer | ||
| footerText={ | ||
| <> | ||
| This email was intended for{" "} | ||
| <span className="text-black">{email}</span>. If you were not | ||
| expecting this email, you can ignore this email. If you have | ||
| any feedback or questions about this email, simply reply to | ||
| it. | ||
| </> | ||
| } | ||
| /> | ||
| </Container> | ||
| </Body> | ||
| </Tailwind> | ||
| </Html> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Confirm composite unique (userId, teamId) exists for userTeam.
findUnique on userId_teamId requires a matching @@unique/@@id. If absent, use findFirst.
🏁 Script executed:
Length of output: 420
Fix findUnique usage — add composite unique or use findFirst
Verified: prisma/schema/team.prisma model UserTeam does NOT define @@unique([userId, teamId]) (composite unique NOT found). findUnique({ where: { userId_teamId: { userId, teamId } } }) is invalid.
🤖 Prompt for AI Agents