diff --git a/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts b/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts index 5bf83bf493..b7f665ff10 100644 --- a/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/NewAlertChannelPresenter.server.ts @@ -1,10 +1,11 @@ import { - AuthenticatableIntegration, + type AuthenticatableIntegration, OrgIntegrationRepository, } from "~/models/orgIntegration.server"; -import { logger } from "~/services/logger.server"; import { BasePresenter } from "./basePresenter.server"; -import { WebClient } from "@slack/web-api"; +import { type WebClient } from "@slack/web-api"; +import { tryCatch } from "@trigger.dev/core"; +import { logger } from "~/services/logger.server"; export class NewAlertChannelPresenter extends BasePresenter { public async call(projectId: string) { @@ -30,42 +31,66 @@ export class NewAlertChannelPresenter extends BasePresenter { // If there is a slack integration, then we need to get a list of Slack Channels if (slackIntegration) { - const channels = await getSlackChannelsForToken(slackIntegration); + const [error, channels] = await tryCatch(getSlackChannelsForToken(slackIntegration)); + + if (error) { + if (isSlackError(error) && error.data.error === "token_revoked") { + return { + slack: { + status: "TOKEN_REVOKED" as const, + }, + }; + } + + if (isSlackError(error) && error.data.error === "token_expired") { + return { + slack: { + status: "TOKEN_EXPIRED" as const, + }, + }; + } + + logger.error("Failed fetching Slack channels for creating alerts", { + error, + slackIntegrationId: slackIntegration.id, + }); + + return { + slack: { + status: "FAILED_FETCHING_CHANNELS" as const, + }, + }; + } return { slack: { status: "READY" as const, - channels, + channels: channels ?? [], integrationId: slackIntegration.id, }, }; - } else { - if (OrgIntegrationRepository.isSlackSupported) { - return { - slack: { - status: "NOT_CONFIGURED" as const, - }, - }; - } else { - return { - slack: { - status: "NOT_AVAILABLE" as const, - }, - }; - } } + + if (OrgIntegrationRepository.isSlackSupported) { + return { + slack: { + status: "NOT_CONFIGURED" as const, + }, + }; + } + + return { + slack: { + status: "NOT_AVAILABLE" as const, + }, + }; } } async function getSlackChannelsForToken(integration: AuthenticatableIntegration) { const client = await OrgIntegrationRepository.getAuthenticatedClientForIntegration(integration); - const channels = await getAllSlackConversations(client); - logger.debug("Received a list of slack conversations", { - channels, - }); - return (channels ?? []) .filter((channel) => !channel.is_archived) .filter((channel) => channel.is_channel) @@ -100,3 +125,15 @@ async function getAllSlackConversations(client: WebClient) { return channels; } + +function isSlackError(obj: unknown): obj is { data: { error: string } } { + return Boolean( + typeof obj === "object" && + obj !== null && + "data" in obj && + typeof obj.data === "object" && + obj.data !== null && + "error" in obj.data && + typeof obj.data.error === "string" + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts index 99dc07f12a..6800ab2ed8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts @@ -1,13 +1,11 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { prisma } from "~/db.server"; -import { env } from "~/env.server"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { OrgIntegrationRepository } from "~/models/orgIntegration.server"; import { findProjectBySlug } from "~/models/project.server"; import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema, - ProjectParamSchema, v3NewProjectAlertPath, v3NewProjectAlertPathConnectToSlackPath, } from "~/utils/pathBuilder"; @@ -16,6 +14,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await requireUserId(request); const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + const url = new URL(request.url); + const shouldReinstall = url.searchParams.get("reinstall") === "true"; + const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { @@ -30,7 +31,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }, }); - if (integration) { + // If integration exists and we're not reinstalling, redirect back to alerts + if (integration && !shouldReinstall) { return redirectWithSuccessMessage( `${v3NewProjectAlertPath({ slug: organizationSlug }, project, { slug: envParam, @@ -38,15 +40,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { request, "Successfully connected your Slack workspace" ); - } else { - // Redirect to Slack - return await OrgIntegrationRepository.redirectToAuthService( - "SLACK", - project.organizationId, - request, - v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, { - slug: envParam, - }) - ); } + + // Redirect to Slack for new installation or reinstallation + return await OrgIntegrationRepository.redirectToAuthService( + "SLACK", + project.organizationId, + request, + v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, { + slug: envParam, + }) + ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 526798cd78..18ef2e522e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -356,6 +356,31 @@ export default function Page() { Connect to Slack + ) : slack.status === "TOKEN_REVOKED" || slack.status === "TOKEN_EXPIRED" ? ( +
+ + The Slack integration in your workspace has been revoked or has expired. + Please re-connect your Slack workspace. + + + + Connect to Slack + + +
+ ) : slack.status === "FAILED_FETCHING_CHANNELS" ? ( +
+ + Failed loading channels from Slack. Please try again later. + +
) : ( Slack integration is not available. Please contact your organization diff --git a/apps/webapp/app/v3/services/createOrgIntegration.server.ts b/apps/webapp/app/v3/services/createOrgIntegration.server.ts index 4db6581c9a..4f33a58fe8 100644 --- a/apps/webapp/app/v3/services/createOrgIntegration.server.ts +++ b/apps/webapp/app/v3/services/createOrgIntegration.server.ts @@ -1,10 +1,5 @@ import { OrganizationIntegration } from "@trigger.dev/database"; import { BaseService } from "./baseService.server"; -import { WebClient } from "@slack/web-api"; -import { env } from "~/env.server"; -import { $transaction } from "~/db.server"; -import { getSecretStore } from "~/services/secrets/secretStore.server"; -import { generateFriendlyId } from "../friendlyIdentifiers"; import { OrgIntegrationRepository } from "~/models/orgIntegration.server"; export class CreateOrgIntegrationService extends BaseService {