Skip to content
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

Feature/App Store + Slack #262

Merged
merged 12 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
  • Loading branch information
pontusab committed Sep 26, 2024
commit 92a0265010fd11835bfa93b4f36b35aaa7015af4
31 changes: 31 additions & 0 deletions apps/dashboard/src/actions/disconnect-app-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use server";

import { LogEvents } from "@midday/events/events";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { authActionClient } from "./safe-action";

export const disconnectAppAction = authActionClient
.schema(
z.object({
appId: z.string(),
}),
)
.metadata({
name: "disconnect-app",
track: {
event: LogEvents.DisconnectApp.name,
channel: LogEvents.DisconnectApp.channel,
},
})
.action(async ({ parsedInput: { appId }, ctx: { supabase } }) => {
const { data } = await supabase
.from("apps")
.delete()
.eq("app_id", appId)
.select();

revalidatePath("/apps");

return data;
});
14 changes: 12 additions & 2 deletions apps/dashboard/src/app/[locale]/(app)/(sidebar)/apps/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Apps } from "@/components/apps";
import { AppsHeader } from "@/components/apps-header";
import { AppsServer } from "@/components/apps.server";
import { getUser } from "@midday/supabase/cached-queries";
import type { Metadata } from "next";
import { Suspense } from "react";

export const metadata: Metadata = {
title: "Apps | Midday",
Expand All @@ -9,5 +11,13 @@ export const metadata: Metadata = {
export default async function Page() {
const { data } = await getUser();

return <Apps user={data} />;
return (
<div className="mt-4">
<AppsHeader />

<Suspense fallback={<div>Loading...</div>}>
<AppsServer user={data} />
</Suspense>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { useEffect } from "react";
import type { WindowEvent } from "./schema";

type Props = {
event: WindowEvent;
};

export const EventEmitter = ({ event }: Props) => {
useEffect(() => {
if (!window?.opener) {
return;
}

if (event) {
window.opener.postMessage(event, "*");
}
}, [event]);

return null;
};
26 changes: 26 additions & 0 deletions apps/dashboard/src/app/[locale]/(public)/all-done/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { notFound } from "next/navigation";
import { EventEmitter } from "./event-emitter";
import { searchParamsSchema } from "./schema";

type Props = {
searchParams: Record<string, string | string[] | undefined>;
};

const AllDonePage = ({ searchParams }: Props) => {
const parsedSearchParams = searchParamsSchema.safeParse(searchParams);

if (!parsedSearchParams.success) {
notFound();
}

return (
<>
<EventEmitter event={parsedSearchParams.data.event} />
<div className="w-dvw h-dvh flex items-center justify-center">
<p>All done, you can close this window!</p>
</div>
</>
);
};

export default AllDonePage;
7 changes: 7 additions & 0 deletions apps/dashboard/src/app/[locale]/(public)/all-done/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const searchParamsSchema = z.object({
event: z.literal("slack_oauth_completed"),
});

export type WindowEvent = z.infer<typeof searchParamsSchema>["event"];
63 changes: 63 additions & 0 deletions apps/dashboard/src/app/api/apps/slack/events/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { config, handleSlackEvent } from "@midday/apps/slack";
import { createClient } from "@midday/supabase/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { NextResponse } from "next/server";

const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(15, "1m"),
analytics: true,
});

export async function POST(req: Request) {
const { challenge, team_id, event } = await req.json();

const ip = req.headers.get("x-forwarded-for") ?? "127.0.0.1";
const { success } = await ratelimit.limit(ip);

if (!success) {
return NextResponse.json(
{ error: "Too many requests" },
{
status: 429,
},
);
}

const supabase = createClient({ admin: true });

const { data } = await supabase
.from("apps")
.select("team_id, settings")
.eq("app_id", config.id)
.eq("settings->>team_id", team_id)
.single();

if (!data) {
console.error(`No team found for Slack team_id: ${team_id}`);
return NextResponse.json(
{ error: "Unauthorized: No matching team found" },
{ status: 401 },
);
}

const settings = data?.settings as {
access_token: string;
bot_user_id: string;
};

if (challenge) {
return new NextResponse(challenge);
}

if (event) {
await handleSlackEvent(event, {
token: settings.access_token,
});
}

return NextResponse.json({
success: true,
});
}
98 changes: 18 additions & 80 deletions apps/dashboard/src/app/api/apps/slack/oauth_callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createApp } from "@midday/apps/db";
import { config, createSlackApp, slackInstaller } from "@midday/apps/slack";
import { config, createSlackApp } from "@midday/apps/slack";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";

Expand Down Expand Up @@ -50,20 +50,6 @@ export async function GET(request: NextRequest) {
JSON.parse(parsedParams.data.state),
);

// const veryfiedState = await slackInstaller.stateStore?.verifyStateParam(
// new Date(),
// parsedParams.data.state,
// );

// const parsedMetadata = metadataSchema.safeParse(
// JSON.parse(veryfiedState?.metadata ?? "{}"),
// );

// if (!parsedMetadata.success) {
// console.error("Invalid metadata", parsedMetadata.error.errors);
// return NextResponse.json({ error: "Invalid metadata" }, { status: 400 });
// }

try {
const slackOauthAccessUrl = [
"https://slack.com/api/oauth.v2.access",
Expand All @@ -89,10 +75,9 @@ export async function GET(request: NextRequest) {
);
}

console.log(parsedMetadata);

const createdSlackIntegration = await createApp({
team_id: parsedMetadata.data.teamId,
created_by: parsedMetadata.data.userId,
app_id: config.id,
settings: {
access_token: parsedJson.data.access_token,
Expand All @@ -106,75 +91,28 @@ export async function GET(request: NextRequest) {
},
});

// const createdSlackIntegration = await prisma.integrationSlack
// .upsert({
// where: {
// team_id: parsedMetadata.data.teamId,
// team: {
// members: {
// some: {
// user_id: parsedMetadata.data.userId,
// },
// },
// },
// },
// update: {},
// create: {
// team_id: parsedMetadata.data.teamId,
// slack_access_token: parsedJson.data.access_token,
// slack_team_id: parsedJson.data.team.id,
// slack_team_name: parsedJson.data.team.name,
// slack_channel: parsedJson.data.incoming_webhook.channel,
// slack_channel_id: parsedJson.data.incoming_webhook.channel_id,
// slack_configuration_url:
// parsedJson.data.incoming_webhook.configuration_url,
// slack_url: parsedJson.data.incoming_webhook.url,
// slack_bot_user_id: parsedJson.data.bot_user_id,
// },
// select: {
// slack_access_token: true,
// slack_bot_user_id: true,
// slack_channel_id: true,
// team: {
// select: {
// id: true,
// name: true,
// },
// },
// },
// })
// .catch((_err) => {
// throw new Error("Failed to create slack integration");
// });

if (true) {
// const slackApp = createSlackApp({
// token: createdSlackIntegration.slack_access_token,
// botId: createdSlackIntegration.slack_bot_user_id,
// });

// slackApp.client.chat.postMessage({
// channel: createdSlackIntegration.slack_channel_id,
// text: `👋 Hello, I'm Seventy Seven. I'll send notifications in this channel for the *${createdSlackIntegration.team.name}* team`,
// });

// const requestUrl = new URL(request.url);

// if (process.env.NODE_ENV === "development") {
// requestUrl.protocol = "http";
// }

// analyticsClient.event("slack_integration_complete", {
// team_id: createdSlackIntegration.team.id,
// profileId: parsedMetadata.data.userId,
// });
if (createdSlackIntegration) {
const slackApp = createSlackApp({
token: createdSlackIntegration.settings.access_token,
botId: createdSlackIntegration.settings.bot_user_id,
});

slackApp.client.chat.postMessage({
channel: createdSlackIntegration.settings.channel_id,
text: `Hello, I'm Midday Assistant. I'll send notifications in this channel`,
});

const requestUrl = new URL(request.url);

if (process.env.NODE_ENV === "development") {
requestUrl.protocol = "http";
}

// This window will be in a popup so we redirect to the all-done route which closes the window
// and then sends a browser event to the parent window. Actions can be taken based on this event.
return NextResponse.redirect(
`${requestUrl.origin}/all-done?event=slack_oauth_completed`,
);
// return NextResponse.redirect(`${requestUrl.origin}/settings/integrations`)
}
} catch (err) {
console.error(err);
Expand Down
Loading
Loading