-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[VPAT-51] fix: update workspace invitation flow to use token for validation #8508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Conversation
- 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.
|
Linked to Plane Work Item(s) This comment was auto-generated by Plane |
📝 WalkthroughWalkthroughThe pull request replaces email-based validation with token-based validation in workspace invitations. Changes span the invite URL generation in serializers, the workspace join endpoint validation logic, background tasks that generate invitation URLs, and the frontend workspace invitations page. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/Frontend
participant API as Backend API
participant DB as Workspace Invite
User->>API: POST /workspace-invitations/join<br/>(invitation_id, slug, token)
API->>DB: Fetch workspace_invite by ID
DB-->>API: Return invitation record
alt Token Valid
API->>API: Extract email from workspace_invite
API->>API: Create/Update WorkspaceMember
API-->>User: Success response
else Token Invalid/Missing
API-->>User: Forbidden (403)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request updates the workspace invitation flow from email-based validation to token-based validation, significantly enhancing security by preventing unauthorized users from accepting invitations simply by knowing the email address.
Key Changes:
- Frontend now extracts and uses a token parameter from the URL instead of email for invitation validation
- Backend API validates invitations using the token field instead of the email field
- Invitation links now include the token parameter in the URL
- Error handling improved with explicit typing for caught errors
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| apps/web/app/(all)/workspace-invitations/page.tsx | Updated to extract token from URL params, send token in API calls instead of email, improved error typing, removed unused React import |
| apps/api/plane/app/views/workspace/invite.py | Modified WorkspaceJoinEndpoint to validate token instead of email, updated user lookup to use workspace_invite.email |
| apps/api/plane/bgtasks/workspace_invitation_task.py | Updated invitation link generation to include token parameter instead of email |
| apps/api/plane/app/serializers/workspace.py | Modified get_invite_link method to generate links with token parameter |
Comments suppressed due to low confidence (1)
apps/api/plane/app/views/workspace/invite.py:169
- The change from email-based to token-based validation is a significant security enhancement, but there are no tests covering this critical authentication flow. Given that the repository has comprehensive test coverage for other API endpoints (as seen in the tests directory), consider adding tests to verify: 1) token validation works correctly, 2) invalid tokens are rejected with appropriate error messages, 3) missing tokens are handled properly, and 4) the token cannot be reused after the invitation is accepted or rejected.
token = request.data.get("token", "")
# 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,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const invitation_id = searchParams.get("invitation_id"); | ||
| const email = searchParams.get("email"); | ||
| const slug = searchParams.get("slug"); | ||
| const token = searchParams.get("token"); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
| ) | ||
| @invalidate_cache(path="/api/users/me/settings/", multiple=True) | ||
| def post(self, request, slug, pk): | ||
| workspace_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug) |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
| # 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"}, |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
| {"error": "You do not have permission to join the workspace"}, | |
| {"error": "Invalid or expired invitation 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: |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
| if (!invitationDetail) return; | ||
| workspaceService | ||
| if (!invitationDetail || !token) return; | ||
| void workspaceService |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
apps/web/app/(all)/workspace-invitations/page.tsx (1)
42-70: Consider adding user-facing error feedback.Both
handleAcceptandhandleRejectcatch errors but only log them to the console. Users won't see any feedback if the invitation acceptance/rejection fails (e.g., due to network issues, expired invitations, or invalid tokens).💡 Suggested improvement for error handling
Consider using a toast notification or error state to inform users when operations fail:
const handleAccept = () => { if (!invitationDetail) return; workspaceService .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { accepted: true, token: token, }) .then(() => { if (invitationDetail.email === currentUser?.email) { router.push(`/${invitationDetail.workspace.slug}`); } else { router.push("/"); } }) - .catch((err: unknown) => console.error(err)); + .catch((err: unknown) => { + console.error(err); + // Show user-facing error message + setToastAlert({ + type: TOAST_TYPE.ERROR, + title: "Error", + message: "Failed to accept invitation. Please try again.", + }); + }); };
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/api/plane/app/serializers/workspace.pyapps/api/plane/app/views/workspace/invite.pyapps/api/plane/bgtasks/workspace_invitation_task.pyapps/web/app/(all)/workspace-invitations/page.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,mts,cts}
📄 CodeRabbit inference engine (.github/instructions/typescript.instructions.md)
**/*.{ts,tsx,mts,cts}: Useconsttype parameters for more precise literal inference in TypeScript 5.0+
Use thesatisfiesoperator to validate types without widening them
Leverage inferred type predicates to reduce the need for explicitisreturn types in filter/check functions
UseNoInfer<T>utility to block inference for specific type arguments when they should be determined by other arguments
Utilize narrowing inswitch(true)blocks for control flow analysis (TypeScript 5.3+)
Rely on narrowing from direct boolean comparisons for type guards
Trust preserved narrowing in closures when variables aren't modified after the check (TypeScript 5.4+)
Use constant indices to narrow object/array properties (TypeScript 5.5+)
Use standard ECMAScript decorators (Stage 3) instead of legacyexperimentalDecorators
Useusingdeclarations for explicit resource management with Disposable pattern instead of manual cleanup (TypeScript 5.2+)
Usewith { type: "json" }for import attributes; avoid deprecatedassertsyntax (TypeScript 5.3/5.8+)
Useimport typeexplicitly when importing types to ensure they are erased during compilation, respectingverbatimModuleSyntaxflag
Use.ts,.mts,.ctsextensions inimport typestatements (TypeScript 5.2+)
Useimport type { Type } from "mod" with { "resolution-mode": "import" }for specific module resolution contexts (TypeScript 5.3+)
Use new iterator methods (map, filter, etc.) if targeting modern environments (TypeScript 5.6+)
Utilize newSetmethods likeunion,intersection, etc., when available (TypeScript 5.5+)
UseObject.groupBy/Map.groupBystandard methods for grouping instead of external libraries (TypeScript 5.4+)
UsePromise.withResolvers()for creating promises with exposed resolve/reject functions (TypeScript 5.7+)
Use copying array methods (toSorted,toSpliced,with) for immutable array operations (TypeScript 5.2+)
Avoid accessing instance fields viasuperin classes (TypeScript 5....
Files:
apps/web/app/(all)/workspace-invitations/page.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Enable TypeScript strict mode and ensure all files are fully typed
Files:
apps/web/app/(all)/workspace-invitations/page.tsx
**/*.{js,jsx,ts,tsx,json,css}
📄 CodeRabbit inference engine (AGENTS.md)
Use Prettier with Tailwind plugin for code formatting, run
pnpm fix:format
Files:
apps/web/app/(all)/workspace-invitations/page.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,jsx,ts,tsx}: Use ESLint with shared config across packages, adhering to max warnings limits per package
Use camelCase for variable and function names, PascalCase for components and types
Use try-catch with proper error types and log errors appropriately
Files:
apps/web/app/(all)/workspace-invitations/page.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
📚 Learning: 2025-07-23T18:18:06.875Z
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
Applied to files:
apps/api/plane/app/views/workspace/invite.pyapps/api/plane/app/serializers/workspace.py
📚 Learning: 2025-12-23T14:18:32.899Z
Learnt from: dheeru0198
Repo: makeplane/plane PR: 8339
File: apps/api/plane/db/models/api.py:35-35
Timestamp: 2025-12-23T14:18:32.899Z
Learning: Django REST Framework rate limit strings are flexible: only the first character of the time unit matters. Acceptable formats include: "60/s", "60/sec", "60/second" (all equivalent), "60/m", "60/min", "60/minute" (all equivalent), "60/h", "60/hr", "60/hour" (all equivalent), and "60/d", "60/day" (all equivalent). Abbreviations like "min" are valid and do not need to be changed to "minute". Apply this guidance to any Python files in the project that configure DRF throttling rules.
Applied to files:
apps/api/plane/app/views/workspace/invite.pyapps/api/plane/app/serializers/workspace.pyapps/api/plane/bgtasks/workspace_invitation_task.py
📚 Learning: 2025-10-21T17:22:05.204Z
Learnt from: lifeiscontent
Repo: makeplane/plane PR: 7989
File: apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx:45-46
Timestamp: 2025-10-21T17:22:05.204Z
Learning: In the makeplane/plane repository, the refactor from useParams() to params prop is specifically scoped to page.tsx and layout.tsx files in apps/web/app (Next.js App Router pattern). Other components (hooks, regular client components, utilities) should continue using the useParams() hook as that is the correct pattern for non-route components.
Applied to files:
apps/web/app/(all)/workspace-invitations/page.tsx
🧬 Code graph analysis (1)
apps/api/plane/app/views/workspace/invite.py (1)
apps/api/plane/db/models/user.py (1)
User(42-183)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Lint API
- GitHub Check: Agent
- GitHub Check: CodeQL analysis (javascript-typescript)
- GitHub Check: Analyze (javascript)
- GitHub Check: Build packages
🔇 Additional comments (8)
apps/api/plane/bgtasks/workspace_invitation_task.py (1)
28-28: LGTM! Invitation URL updated correctly.The invitation URL now includes the token parameter instead of email, which enhances security by avoiding exposure of email addresses in URLs while maintaining proper validation on the backend.
apps/api/plane/app/serializers/workspace.py (1)
109-110: LGTM! Serializer updated correctly for token-based invitations.The invite link now includes the token parameter instead of email, aligning with the enhanced security approach across the invitation flow.
apps/api/plane/app/views/workspace/invite.py (3)
162-169: Excellent security improvement with token-based validation!The token validation correctly ensures that only users who received the invitation email can access the workspace invitation flow. The check for both token presence and equality is appropriate.
179-179: Good security practice using invitation email.Using
workspace_invite.emailfor user lookup after token validation ensures the email comes from the validated invitation record rather than potentially malicious user input.
234-237: The GET endpoint intentionally allows unauthenticated access to invitation details.The endpoint exposes workspace metadata (name, role, email) without token validation. This is by design: the frontend calls
getWorkspaceInvitation()to display invitation details to unauthenticated users before they log in or create an account. Token validation is only required for the POST endpoint (accepting/rejecting the invitation), not for viewing.Since the serializer already exposes the token in the response (via
invite_link), requiring token on the GET would provide no additional security and would break the UX pattern. If you have concerns about enumerating invitations by guessing IDs, consider adding rate limiting or allowing only pre-registered email addresses to view invitations.apps/web/app/(all)/workspace-invitations/page.tsx (3)
31-31: LGTM! Token properly retrieved from query params.The token is correctly extracted from the URL query parameters for validation in subsequent API calls.
42-57: Token-based acceptance flow implemented correctly.The accept flow now:
- Validates invitation details exist before proceeding
- Includes the token in the API payload (line 47) for server-side validation
- Uses server-provided email data for comparison (line 50) rather than query params
- Properly types errors as
unknown(line 56)
59-70: Good defensive programming with token guard.The reject flow correctly validates both
invitationDetailandtokenpresence before making the API call. Thevoidprefix on line 61 explicitly documents that the promise result is intentionally ignored.
Description
Type of Change
Screenshots and Media (if applicable)
Test Scenarios
References
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.