diff --git a/apps/api/src/app/api/integrations/slack/events/process-app-home-opened/process-app-home-opened.ts b/apps/api/src/app/api/integrations/slack/events/process-app-home-opened/process-app-home-opened.ts index 5d5311433d3..3b79a195959 100644 --- a/apps/api/src/app/api/integrations/slack/events/process-app-home-opened/process-app-home-opened.ts +++ b/apps/api/src/app/api/integrations/slack/events/process-app-home-opened/process-app-home-opened.ts @@ -1,8 +1,7 @@ -import { createHmac } from "node:crypto"; import { db } from "@superset/db/client"; import { integrationConnections, usersSlackUsers } from "@superset/db/schema"; import { and, eq } from "drizzle-orm"; -import { env } from "@/env"; +import { generateConnectUrl } from "../utils/generate-connect-url"; import { createSlackClient } from "../utils/slack-client"; import { buildHomeView } from "./build-home-view"; @@ -12,25 +11,6 @@ interface ProcessAppHomeOpenedParams { eventId: string; } -function generateConnectUrl({ - slackUserId, - teamId, -}: { - slackUserId: string; - teamId: string; -}): string { - const payload = JSON.stringify({ - slackUserId, - teamId, - exp: Date.now() + 10 * 60 * 1000, - }); - const signature = createHmac("sha256", env.SLACK_SIGNING_SECRET) - .update(payload) - .digest("hex"); - const token = Buffer.from(payload).toString("base64url"); - return `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/link?token=${token}&sig=${signature}`; -} - export async function processAppHomeOpened({ event, teamId, diff --git a/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts b/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts index 55e71727beb..75cf457d103 100644 --- a/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts +++ b/apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts @@ -1,7 +1,12 @@ import type { GenericMessageEvent } from "@slack/types"; import { db } from "@superset/db/client"; -import { integrationConnections, usersSlackUsers } from "@superset/db/schema"; +import { + integrationConnections, + subscriptions, + usersSlackUsers, +} from "@superset/db/schema"; import { and, eq } from "drizzle-orm"; +import { generateConnectUrl } from "../utils/generate-connect-url"; import { formatErrorForSlack, resolveUserMentions, @@ -45,15 +50,91 @@ export async function processAssistantMessage({ const slack = createSlackClient(connection.accessToken); - const slackUserLink = event.user - ? await db.query.usersSlackUsers.findFirst({ - where: and( - eq(usersSlackUsers.slackUserId, event.user), - eq(usersSlackUsers.teamId, teamId), - ), - columns: { modelPreference: true }, - }) - : undefined; + const [slackUserLink, activeSubscription] = await Promise.all([ + event.user + ? db.query.usersSlackUsers.findFirst({ + where: and( + eq(usersSlackUsers.slackUserId, event.user), + eq(usersSlackUsers.teamId, teamId), + ), + columns: { userId: true, modelPreference: true }, + }) + : undefined, + db.query.subscriptions.findFirst({ + where: and( + eq(subscriptions.referenceId, connection.organizationId), + eq(subscriptions.status, "active"), + ), + columns: { id: true }, + }), + ]); + + if (!activeSubscription) { + await slack.chat.postMessage({ + channel: event.channel, + thread_ts: event.thread_ts ?? event.ts, + text: "The Superset Slack integration requires a Pro plan.", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "The Superset Slack integration requires a Pro plan.", + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "Upgrade to Pro", emoji: true }, + url: "https://app.superset.sh/settings/billing", + style: "primary", + }, + ], + }, + ], + }); + return; + } + + if (!slackUserLink) { + if (!event.user) return; + const connectUrl = generateConnectUrl({ + slackUserId: event.user, + teamId, + }); + await slack.chat.postMessage({ + channel: event.channel, + thread_ts: event.thread_ts ?? event.ts, + text: "To use Superset, you need to link your Slack account first.", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "To use Superset, you need to link your Slack account first.", + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Connect Account", + emoji: true, + }, + url: connectUrl, + style: "primary", + }, + ], + }, + ], + }); + return; + } const threadTs = event.thread_ts ?? event.ts; @@ -84,8 +165,9 @@ export async function processAssistantMessage({ channelId: event.channel, threadTs, organizationId: connection.organizationId, + userId: slackUserLink.userId, slackToken: connection.accessToken, - model: slackUserLink?.modelPreference ?? undefined, + model: slackUserLink.modelPreference ?? undefined, onProgress: messageTs ? async (status) => { try { diff --git a/apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts b/apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts index ce0745d4c50..e60e691fec9 100644 --- a/apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts +++ b/apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts @@ -1,7 +1,12 @@ import type { AppMentionEvent } from "@slack/types"; import { db } from "@superset/db/client"; -import { integrationConnections, usersSlackUsers } from "@superset/db/schema"; +import { + integrationConnections, + subscriptions, + usersSlackUsers, +} from "@superset/db/schema"; import { and, eq } from "drizzle-orm"; +import { generateConnectUrl } from "../utils/generate-connect-url"; import { formatErrorForSlack, resolveUserMentions, @@ -45,15 +50,91 @@ export async function processSlackMention({ const slack = createSlackClient(connection.accessToken); - const slackUserLink = event.user - ? await db.query.usersSlackUsers.findFirst({ - where: and( - eq(usersSlackUsers.slackUserId, event.user), - eq(usersSlackUsers.teamId, teamId), - ), - columns: { modelPreference: true }, - }) - : undefined; + const [slackUserLink, activeSubscription] = await Promise.all([ + event.user + ? db.query.usersSlackUsers.findFirst({ + where: and( + eq(usersSlackUsers.slackUserId, event.user), + eq(usersSlackUsers.teamId, teamId), + ), + columns: { userId: true, modelPreference: true }, + }) + : undefined, + db.query.subscriptions.findFirst({ + where: and( + eq(subscriptions.referenceId, connection.organizationId), + eq(subscriptions.status, "active"), + ), + columns: { id: true }, + }), + ]); + + if (!activeSubscription) { + await slack.chat.postMessage({ + channel: event.channel, + thread_ts: event.thread_ts ?? event.ts, + text: "The Superset Slack integration requires a Pro plan.", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "The Superset Slack integration requires a Pro plan.", + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "Upgrade to Pro", emoji: true }, + url: "https://app.superset.sh/settings/billing", + style: "primary", + }, + ], + }, + ], + }); + return; + } + + if (!slackUserLink) { + if (!event.user) return; + const connectUrl = generateConnectUrl({ + slackUserId: event.user, + teamId, + }); + await slack.chat.postMessage({ + channel: event.channel, + thread_ts: event.thread_ts ?? event.ts, + text: "To use Superset, you need to link your Slack account first.", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "To use Superset, you need to link your Slack account first.", + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Connect Account", + emoji: true, + }, + url: connectUrl, + style: "primary", + }, + ], + }, + ], + }); + return; + } try { await slack.reactions.add({ @@ -94,8 +175,9 @@ export async function processSlackMention({ channelId: event.channel, threadTs, organizationId: connection.organizationId, + userId: slackUserLink.userId, slackToken: connection.accessToken, - model: slackUserLink?.modelPreference ?? undefined, + model: slackUserLink.modelPreference ?? undefined, onProgress: messageTs ? async (status) => { try { diff --git a/apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts b/apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts new file mode 100644 index 00000000000..323e21eeb71 --- /dev/null +++ b/apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts @@ -0,0 +1,21 @@ +import { createHmac } from "node:crypto"; +import { env } from "@/env"; + +export function generateConnectUrl({ + slackUserId, + teamId, +}: { + slackUserId: string; + teamId: string; +}): string { + const payload = JSON.stringify({ + slackUserId, + teamId, + exp: Date.now() + 10 * 60 * 1000, + }); + const signature = createHmac("sha256", env.SLACK_SIGNING_SECRET) + .update(payload) + .digest("hex"); + const token = Buffer.from(payload).toString("base64url"); + return `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/link?token=${token}&sig=${signature}`; +} diff --git a/apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/index.ts b/apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/index.ts new file mode 100644 index 00000000000..8d457a03db1 --- /dev/null +++ b/apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/index.ts @@ -0,0 +1 @@ +export { generateConnectUrl } from "./generate-connect-url"; diff --git a/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts b/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts index d634f9a2b02..404ae4b4607 100644 --- a/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts +++ b/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts @@ -1,9 +1,6 @@ import Anthropic from "@anthropic-ai/sdk"; import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { WebClient } from "@slack/web-api"; -import { db } from "@superset/db/client"; -import { integrationConnections } from "@superset/db/schema"; -import { and, eq } from "drizzle-orm"; import { env } from "@/env"; import { DEFAULT_SLACK_MODEL } from "../../../constants"; import type { AgentAction } from "../slack-blocks"; @@ -113,6 +110,7 @@ interface RunSlackAgentParams { channelId: string; threadTs: string; organizationId: string; + userId: string; slackToken: string; model?: string; onProgress?: (status: string) => void | Promise; @@ -417,18 +415,6 @@ export async function runSlackAgent( const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); const actions: AgentAction[] = []; - const connection = await db.query.integrationConnections.findFirst({ - where: and( - eq(integrationConnections.organizationId, params.organizationId), - eq(integrationConnections.provider, "slack"), - ), - columns: { connectedByUserId: true }, - }); - - if (!connection) { - throw new Error("Slack connection not found"); - } - let supersetMcp: Client | null = null; let cleanupSuperset: (() => Promise) | null = null; @@ -441,7 +427,7 @@ export async function runSlackAgent( }), createSupersetMcpClient({ organizationId: params.organizationId, - userId: connection.connectedByUserId, + userId: params.userId, }), ]); @@ -452,7 +438,7 @@ export async function runSlackAgent( supersetMcp.listTools(), fetchAgentContext({ mcpClient: supersetMcp, - userId: connection.connectedByUserId, + userId: params.userId, }), ]); diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 9d0c06332b0..dba964a958d 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -1,3 +1,9 @@ +import { + DOWNLOAD_URL_MAC_ARM64, + PROTOCOL_SCHEMES, +} from "@superset/shared/constants"; +import { Button } from "@superset/ui/button"; +import { Download, ExternalLink } from "lucide-react"; import { HiCheckCircle } from "react-icons/hi2"; export default async function BillingPage({ @@ -8,24 +14,41 @@ export default async function BillingPage({ const { success } = await searchParams; const isSuccess = success === "true"; - if (!isSuccess) { + if (isSuccess) { return ( -
+
+ +

Payment Successful

- Manage your billing in the desktop app. + Your subscription has been activated. You can now access all Pro + features.

); } return ( -
- -

Payment Successful

-

- Your subscription has been activated. You can now access all Pro - features. -

+
+
+

Billing

+

+ Manage your subscription and billing in the desktop app. +

+
+
); }