From b372ccfdb351d2ce2c6c2938d5cb823db2db778b Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:00:49 +0530 Subject: [PATCH] fix: slack integration workflow (#2675) * fix: slack integration workflow * dev: add slack client id as configuration * fix: clean up * fix: added env to turbo --------- Co-authored-by: sriram veeraghanta --- apiserver/plane/api/urls/__init__.py | 2 +- .../api/urls/{configuration.py => config.py} | 0 apiserver/plane/api/views/config.py | 1 + apiserver/plane/api/views/integration/base.py | 15 +++- .../plane/api/views/integration/slack.py | 44 ++++++++--- apiserver/plane/utils/integrations/slack.py | 20 +++++ turbo.json | 3 +- web/pages/api/slack-redirect.ts | 23 ------ web/pages/installations/[provider]/index.tsx | 78 ++++++++----------- web/services/app_installation.service.ts | 12 --- 10 files changed, 99 insertions(+), 99 deletions(-) rename apiserver/plane/api/urls/{configuration.py => config.py} (100%) create mode 100644 apiserver/plane/utils/integrations/slack.py delete mode 100644 web/pages/api/slack-redirect.ts diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index 49c2b772e..e4f3718f5 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -1,7 +1,7 @@ from .analytic import urlpatterns as analytic_urls from .asset import urlpatterns as asset_urls from .authentication import urlpatterns as authentication_urls -from .configuration import urlpatterns as configuration_urls +from .config import urlpatterns as configuration_urls from .cycle import urlpatterns as cycle_urls from .estimate import urlpatterns as estimate_urls from .gpt import urlpatterns as gpt_urls diff --git a/apiserver/plane/api/urls/configuration.py b/apiserver/plane/api/urls/config.py similarity index 100% rename from apiserver/plane/api/urls/configuration.py rename to apiserver/plane/api/urls/config.py diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py index f59ca04a0..687cb211c 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/api/views/config.py @@ -30,4 +30,5 @@ class ConfigurationEndpoint(BaseAPIView): data["email_password_login"] = ( os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1" ) + data["slack"] = os.environ.get("SLACK_CLIENT_ID", None) return Response(data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 65b94d0a1..cc911b537 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -1,6 +1,6 @@ # Python improts import uuid - +import requests # Django imports from django.contrib.auth.hashers import make_password @@ -25,7 +25,7 @@ from plane.utils.integrations.github import ( delete_github_installation, ) from plane.api.permissions import WorkSpaceAdminPermission - +from plane.utils.integrations.slack import slack_oauth class IntegrationViewSet(BaseViewSet): serializer_class = IntegrationSerializer @@ -98,12 +98,19 @@ class WorkspaceIntegrationViewSet(BaseViewSet): config = {"installation_id": installation_id} if provider == "slack": - metadata = request.data.get("metadata", {}) + code = request.data.get("code", False) + + if not code: + return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST) + + slack_response = slack_oauth(code=code) + + metadata = slack_response access_token = metadata.get("access_token", False) team_id = metadata.get("team", {}).get("id", False) if not metadata or not access_token or not team_id: return Response( - {"error": "Access token and team id is required"}, + {"error": "Slack could not be installed. Please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) config = {"team_id": team_id, "access_token": access_token} diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py index 83aa951ba..863b6ba0c 100644 --- a/apiserver/plane/api/views/integration/slack.py +++ b/apiserver/plane/api/views/integration/slack.py @@ -11,6 +11,7 @@ from plane.api.views import BaseViewSet, BaseAPIView from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember from plane.api.serializers import SlackProjectSyncSerializer from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.utils.integrations.slack import slack_oauth class SlackProjectSyncViewSet(BaseViewSet): @@ -32,25 +33,46 @@ class SlackProjectSyncViewSet(BaseViewSet): ) def create(self, request, slug, project_id, workspace_integration_id): - serializer = SlackProjectSyncSerializer(data=request.data) + try: + code = request.data.get("code", False) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) + if not code: + return Response( + {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST + ) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - workspace_integration_id=workspace_integration_id, + slack_response = slack_oauth(code=code) + + workspace_integration = WorkspaceIntegration.objects.get( + workspace__slug=slug, pk=workspace_integration_id ) workspace_integration = WorkspaceIntegration.objects.get( pk=workspace_integration_id, workspace__slug=slug ) - + slack_project_sync = SlackProjectSync.objects.create( + access_token=slack_response.get("access_token"), + scopes=slack_response.get("scope"), + bot_user_id=slack_response.get("bot_user_id"), + webhook_url=slack_response.get("incoming_webhook", {}).get("url"), + data=slack_response, + team_id=slack_response.get("team", {}).get("id"), + team_name=slack_response.get("team", {}).get("name"), + workspace_integration=workspace_integration, + ) _ = ProjectMember.objects.get_or_create( member=workspace_integration.actor, role=20, project_id=project_id ) - + serializer = SlackProjectSyncSerializer(slack_project_sync) return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"error": "Slack is already installed for the project"}, + status=status.HTTP_410_GONE, + ) + capture_exception(e) + return Response( + {"error": "Slack could not be installed. Please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py new file mode 100644 index 000000000..70f26e160 --- /dev/null +++ b/apiserver/plane/utils/integrations/slack.py @@ -0,0 +1,20 @@ +import os +import requests + +def slack_oauth(code): + SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False) + SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False) + SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False) + + # Oauth Slack + if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET: + response = requests.get( + SLACK_OAUTH_URL, + params={ + "code": code, + "client_id": SLACK_CLIENT_ID, + "client_secret": SLACK_CLIENT_SECRET, + }, + ) + return response.json() + return {} diff --git a/turbo.json b/turbo.json index 62afa90bb..7c3ccb81a 100644 --- a/turbo.json +++ b/turbo.json @@ -22,7 +22,8 @@ "SLACK_CLIENT_SECRET", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST", - "UNSPLASH_ACCESS_KEY" + "UNSPLASH_ACCESS_KEY", + "NEXT_PUBLIC_SLACK_CLIENT_ID" ], "pipeline": { "build": { diff --git a/web/pages/api/slack-redirect.ts b/web/pages/api/slack-redirect.ts deleted file mode 100644 index a6b8dbf4b..000000000 --- a/web/pages/api/slack-redirect.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from "axios"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) { - try { - const { code } = req.body; - - if (!code || code === "") return res.status(400).json({ message: "Code is required" }); - - const response = await axios({ - method: "post", - url: process.env.SLACK_OAUTH_URL || "", - params: { - client_id: process.env.SLACK_CLIENT_ID, - client_secret: process.env.SLACK_CLIENT_SECRET, - code, - }, - }); - res.status(200).json(response?.data); - } catch (error) { - res.status(200).json({ message: "Internal Server Error" }); - } -} diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index ac8a2fc22..243065a7c 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -12,7 +12,7 @@ const appInstallationService = new AppInstallationService(); const AppPostInstallation: NextPageWithLayout = () => { const router = useRouter(); - const { installation_id, setup_action, state, provider, code } = router.query; + const { installation_id, state, provider, code } = router.query; useEffect(() => { if (provider === "github" && state && installation_id) { @@ -27,53 +27,37 @@ const AppPostInstallation: NextPageWithLayout = () => { console.log(err); }); } else if (provider === "slack" && state && code) { - appInstallationService - .getSlackAuthDetails(code.toString()) - .then((res) => { - const [workspaceSlug, projectId, integrationId] = state.toString().split(","); + const [workspaceSlug, projectId, integrationId] = state.toString().split(","); - if (!projectId) { - const payload = { - metadata: { - ...res, - }, - }; - - appInstallationService - .addInstallationApp(state.toString(), provider, payload) - .then((r) => { - window.opener = null; - window.open("", "_self"); - window.close(); - }) - .catch((err) => { - throw err?.response; - }); - } else { - const payload = { - access_token: res.access_token, - bot_user_id: res.bot_user_id, - webhook_url: res.incoming_webhook.url, - data: res, - team_id: res.team.id, - team_name: res.team.name, - scopes: res.scope, - }; - appInstallationService - .addSlackChannel(workspaceSlug, projectId, integrationId, payload) - .then((r) => { - window.opener = null; - window.open("", "_self"); - window.close(); - }) - .catch((err) => { - throw err.response; - }); - } - }) - .catch((err) => { - console.log(err); - }); + if (!projectId) { + const payload = { + code, + }; + appInstallationService + .addInstallationApp(state.toString(), provider, payload) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + throw err?.response; + }); + } else { + const payload = { + code, + }; + appInstallationService + .addSlackChannel(workspaceSlug, projectId, integrationId, payload) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + throw err.response; + }); + } } }, [state, installation_id, provider, code]); diff --git a/web/services/app_installation.service.ts b/web/services/app_installation.service.ts index 2a7a4ea6a..179721036 100644 --- a/web/services/app_installation.service.ts +++ b/web/services/app_installation.service.ts @@ -60,16 +60,4 @@ export class AppInstallationService extends APIService { throw error?.response; }); } - - async getSlackAuthDetails(code: string): Promise { - const response = await this.request({ - method: "post", - url: "/api/slack-redirect", - data: { - code, - }, - }); - - return response.data; - } }