feat: api v2 team invite link endpoint#85
Conversation
Code Review by Qodo
1. Invite creation lacks admin check
|
| @Post("/invite") | ||
| @Roles("TEAM_MEMBER") | ||
| @ApiOperation({ summary: "Create team invite link" }) | ||
| @HttpCode(HttpStatus.OK) | ||
| async createInvite( | ||
| @Param("teamId", ParseIntPipe) teamId: number | ||
| ): Promise<CreateInviteOutputDto> { | ||
| const result = await TeamService.createInvite(teamId); | ||
| return { status: SUCCESS_STATUS, data: result }; |
There was a problem hiding this comment.
1. Invite creation lacks admin check 📘 Rule violation ⛨ Security
• The new /v2/teams/:teamId/invite endpoint does not capture the authenticated user and does not enforce that the caller is a team admin before generating an invite token. • This enables unauthorized invite-link creation (e.g., any authenticated team member), which is a privilege/permission action and should be explicitly authorized. • Without user context being passed into the invite-creation flow, the system cannot reliably validate the caller’s permissions for the specific teamId.
Agent prompt
## Issue description
The invite-link creation endpoint does not include the authenticated user context and does not enforce team-admin authorization for creating invites.
## Issue Context
Creating an invite link is a sensitive permission-related action. The endpoint currently calls `TeamService.createInvite(teamId)` with no user identity, so per-team authorization cannot be reliably enforced.
## Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[31-40]
- packages/features/ee/teams/services/teamService.ts[55-102]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const token = randomBytes(32).toString("hex"); | ||
| await prisma.verificationToken.create({ | ||
| const newToken = await prisma.verificationToken.create({ | ||
| data: { | ||
| identifier: `invite-link-for-teamId-${teamId}`, | ||
| token, |
There was a problem hiding this comment.
2. token returns identifier 📘 Rule violation ⛨ Security
• TeamService.createInvite returns token: newToken.identifier even though the actual secret token is stored in newToken.token. • This is misleading/self-undermining API design: the token field does not contain a token, and it can leak an internal identifier pattern (invite-link-for-teamId-<id>), which is not intended as a secret. • Clients may store/log the returned token thinking it is the invite token, causing broken flows and unnecessary exposure of internal identifiers.
Agent prompt
## Issue description
`TeamService.createInvite` returns `token: newToken.identifier` instead of returning the actual secret token.
## Issue Context
The API response field is named `token` and is expected to be the invitation token. Returning a predictable identifier both misleads consumers and may expose internal patterns.
## Fix Focus Areas
- packages/features/ee/teams/services/teamService.ts[87-101]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| import { API_VERSIONS_VALUES } from "@/lib/api-versions"; | ||
| import { API_KEY_HEADER } from "@/lib/docs/headers"; | ||
| import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; | ||
| import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; | ||
| import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; | ||
| import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; | ||
| import { CreateInviteOutputDto } from "@/modules/teams/invite/outputs/invite.output"; | ||
|
|
||
| import { | ||
| Controller, | ||
| UseGuards, | ||
| Post, | ||
| Param, | ||
| ParseIntPipe, | ||
| HttpCode, | ||
| HttpStatus, | ||
| } from "@nestjs/common"; | ||
| import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; | ||
|
|
||
| import { SUCCESS_STATUS } from "@calcom/platform-constants"; | ||
| import { TeamService } from "@calcom/features/ee/teams/services/teamService"; | ||
|
|
||
| @Controller({ | ||
| path: "/v2/teams/:teamId", | ||
| version: API_VERSIONS_VALUES, | ||
| }) | ||
| @UseGuards(ApiAuthGuard, RolesGuard) | ||
| @DocsTags("Teams / Invite") | ||
| @ApiHeader(API_KEY_HEADER) | ||
| export class TeamsInviteController { | ||
| @Post("/invite") | ||
| @Roles("TEAM_MEMBER") | ||
| @ApiOperation({ summary: "Create team invite link" }) | ||
| @HttpCode(HttpStatus.OK) | ||
| async createInvite( | ||
| @Param("teamId", ParseIntPipe) teamId: number | ||
| ): Promise<CreateInviteOutputDto> { | ||
| const result = await TeamService.createInvite(teamId); | ||
| return { status: SUCCESS_STATUS, data: result }; | ||
| } |
There was a problem hiding this comment.
3. Invite creation not audited 📘 Rule violation ⛨ Security
• The new invite-link creation endpoint performs a critical security action (minting an invite token) without emitting an audit log containing who performed the action, what team was targeted, and the outcome. • Without this, incident response and compliance investigations cannot reliably reconstruct invite creation activity. • Additionally, the handler imports GetUser but does not capture a user identity, making it impossible to meet the audit-log requirements for user attribution.
Agent prompt
## Issue description
Invite-link creation is a critical security action but is not audit-logged with user attribution and outcome.
## Issue Context
Audit logs must include who performed the action (user ID), what action occurred (invite creation for a team), and whether it succeeded or failed. Avoid logging sensitive secrets like the invite token.
## Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[1-40]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| private static async buildInviteLink(token: string, isOrgContext: boolean): Promise<string> { | ||
| const teamInviteLink = `${WEBAPP_URL}/teams?token=${token}`; | ||
| if (!isOrgContext) { | ||
| if (isOrgContext) { | ||
| return teamInviteLink; | ||
| } | ||
| const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited(prisma); |
There was a problem hiding this comment.
4. Invite url logic inverted 🐞 Bug ✓ Correctness
• TeamService.buildInviteLink() now returns /teams?token=... for org-context and /signup?token=... for non-org context. • This is backwards relative to how the signup flow detects “org invite by link” (identifier is not an email) and is likely to bypass the expected org invite signup/onboarding experience. • Result: org/team-in-org invite links may send new users to /teams (login-gated) instead of /signup, while non-org team invite links may send users to /signup even though the token identifier semantics don’t match the non-org signup assumptions.
Agent prompt
### Issue description
`buildInviteLink()` uses the org-context boolean to choose between `/teams?token=...` and `/signup?token=...`, but the current condition is inverted, sending org-context invites to `/teams`.
### Issue Context
The signup SSR flow explicitly detects org-invite-by-link in org-context when `verificationToken.identifier` is not an email; that experience requires the user to land on `/signup?token=...`.
### Fix Focus Areas
- packages/features/ee/teams/services/teamService.ts[104-112]
### Suggested change
- Swap the branch back so that:
- if `!isOrgContext` => return `/teams?token=...`
- else => return `/signup?token=...&callbackUrl=...`
- Add/adjust tests for org-context vs non-org-context link generation if missing.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Benchmark PR from qodo-benchmark#668