From ef5d481a190f0f08ee36aa4677e7a660dcef8e4f Mon Sep 17 00:00:00 2001 From: Dheeraj Kumar Ketireddy Date: Tue, 17 Feb 2026 00:02:18 +0530 Subject: [PATCH] [VPAT-51] fix: update workspace invitation flow to use token for validation #8508 - Modified the invite link to include a token for enhanced security. - Updated the WorkspaceJoinEndpoint to validate the token instead of the email. - Adjusted the workspace invitation task to generate links with the token. - Refactored the frontend to handle token in the invitation process. Co-authored-by: sriram veeraghanta --- apps/api/plane/app/serializers/workspace.py | 2 +- apps/api/plane/app/views/workspace/invite.py | 8 ++++---- .../bgtasks/workspace_invitation_task.py | 2 +- .../app/(all)/workspace-invitations/page.tsx | 19 +++++++++---------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/apps/api/plane/app/serializers/workspace.py b/apps/api/plane/app/serializers/workspace.py index d6707815e5..608cdad851 100644 --- a/apps/api/plane/app/serializers/workspace.py +++ b/apps/api/plane/app/serializers/workspace.py @@ -111,7 +111,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer): invite_link = serializers.SerializerMethodField() def get_invite_link(self, obj): - return f"/workspace-invitations/?invitation_id={obj.id}&email={obj.email}&slug={obj.workspace.slug}" + return f"/workspace-invitations/?invitation_id={obj.id}&slug={obj.workspace.slug}&token={obj.token}" class Meta: model = WorkspaceMemberInvite diff --git a/apps/api/plane/app/views/workspace/invite.py b/apps/api/plane/app/views/workspace/invite.py index 175380b3aa..cf2ab795a7 100644 --- a/apps/api/plane/app/views/workspace/invite.py +++ b/apps/api/plane/app/views/workspace/invite.py @@ -163,10 +163,10 @@ class WorkspaceJoinEndpoint(BaseAPIView): def post(self, request, slug, pk): workspace_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug) - email = request.data.get("email", "") + token = request.data.get("token", "") - # Check the email - if email == "" or workspace_invite.email != email: + # Validate the token to verify the user received the invitation email + if not token or workspace_invite.token != token: return Response( {"error": "You do not have permission to join the workspace"}, status=status.HTTP_403_FORBIDDEN, @@ -180,7 +180,7 @@ class WorkspaceJoinEndpoint(BaseAPIView): if workspace_invite.accepted: # Check if the user created account after invitation - user = User.objects.filter(email=email).first() + user = User.objects.filter(email=workspace_invite.email).first() # If the user is present then create the workspace member if user is not None: diff --git a/apps/api/plane/bgtasks/workspace_invitation_task.py b/apps/api/plane/bgtasks/workspace_invitation_task.py index ced17d599d..9e9a17c2f3 100644 --- a/apps/api/plane/bgtasks/workspace_invitation_task.py +++ b/apps/api/plane/bgtasks/workspace_invitation_task.py @@ -29,7 +29,7 @@ def workspace_invitation(email, workspace_id, token, current_site, inviter): # Relative link relative_link = ( - f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}" # noqa: E501 + f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&slug={workspace.slug}&token={token}" # noqa: E501 ) # The complete url including the domain diff --git a/apps/web/app/(all)/workspace-invitations/page.tsx b/apps/web/app/(all)/workspace-invitations/page.tsx index eb9c92128d..6d68136097 100644 --- a/apps/web/app/(all)/workspace-invitations/page.tsx +++ b/apps/web/app/(all)/workspace-invitations/page.tsx @@ -4,7 +4,6 @@ * See the LICENSE file for details. */ -import React from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; import useSWR from "swr"; @@ -34,8 +33,8 @@ function WorkspaceInvitationPage() { // query params const searchParams = useSearchParams(); const invitation_id = searchParams.get("invitation_id"); - const email = searchParams.get("email"); const slug = searchParams.get("slug"); + const token = searchParams.get("token"); // store hooks const { data: currentUser } = useUser(); @@ -51,29 +50,29 @@ function WorkspaceInvitationPage() { workspaceService .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { accepted: true, - email: invitationDetail.email, + token: token, }) .then(() => { - if (email === currentUser?.email) { + if (invitationDetail.email === currentUser?.email) { router.push(`/${invitationDetail.workspace.slug}`); } else { - router.push(`/?${searchParams.toString()}`); + router.push("/"); } }) - .catch((err) => console.error(err)); + .catch((err: unknown) => console.error(err)); }; const handleReject = () => { - if (!invitationDetail) return; - workspaceService + if (!invitationDetail || !token) return; + void workspaceService .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { accepted: false, - email: invitationDetail.email, + token: token, }) .then(() => { router.push("/"); }) - .catch((err) => console.error(err)); + .catch((err: unknown) => console.error(err)); }; return (