Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion apps/api/plane/app/serializers/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,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
Expand Down
8 changes: 4 additions & 4 deletions apps/api/plane/app/views/workspace/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ class WorkspaceJoinEndpoint(BaseAPIView):
def post(self, request, slug, pk):
workspace_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API endpoint lacks proper error handling for the case where the WorkspaceMemberInvite object doesn't exist. If pk or slug are invalid, this will raise a DoesNotExist exception which will result in a 500 error instead of a more appropriate 404 error. Consider using get_object_or_404 or wrapping this in a try-except block to return a proper 404 response.

Copilot uses AI. Check for mistakes.

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:
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token comparison using direct equality (!=) is vulnerable to timing attacks. An attacker could potentially determine the correct token character by character by measuring response times. Consider using Django's constant_time_compare function from django.utils.crypto to perform the token comparison securely.

Copilot uses AI. Check for mistakes.
return Response(
{"error": "You do not have permission to join the workspace"},
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "You do not have permission to join the workspace" is misleading when the token validation fails. This could be due to an invalid token, expired link, or tampered URL, not necessarily a permission issue. Consider using a more specific error message like "Invalid or expired invitation token" to help users understand the actual problem.

Suggested change
{"error": "You do not have permission to join the workspace"},
{"error": "Invalid or expired invitation token"},

Copilot uses AI. Check for mistakes.
status=status.HTTP_403_FORBIDDEN,
Expand All @@ -176,7 +176,7 @@ def post(self, request, slug, pk):

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:
Expand Down
2 changes: 1 addition & 1 deletion apps/api/plane/bgtasks/workspace_invitation_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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
Expand Down
19 changes: 9 additions & 10 deletions apps/web/app/(all)/workspace-invitations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from "react";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import useSWR from "swr";
Expand Down Expand Up @@ -28,8 +27,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");
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token parameter is not being validated or included in the error handling flow. When a user visits the page without a token, they'll still see the invitation UI and only encounter an error when they try to accept or reject. Consider checking for the token early and showing an appropriate error message if it's missing from the URL parameters.

Copilot uses AI. Check for mistakes.
// store hooks
const { data: currentUser } = useUser();

Expand All @@ -45,29 +44,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
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The void operator is used here but not for handleAccept. For consistency and to explicitly indicate that the promise is intentionally not being awaited, consider applying the same pattern to handleAccept or removing it from both if the promises are meant to be fire-and-forget operations.

Copilot uses AI. Check for mistakes.
.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 (
Expand Down
Loading