Skip to content

feat: api v2 team invite link endpoint#85

Open
tomerqodo wants to merge 2 commits into
qodo_combined_20260121_qodo_grep_cursor_copilot_1_base_feat_api_v2_team_invite_link_endpoint_pr668from
qodo_combined_20260121_qodo_grep_cursor_copilot_1_head_feat_api_v2_team_invite_link_endpoint_pr668
Open

feat: api v2 team invite link endpoint#85
tomerqodo wants to merge 2 commits into
qodo_combined_20260121_qodo_grep_cursor_copilot_1_base_feat_api_v2_team_invite_link_endpoint_pr668from
qodo_combined_20260121_qodo_grep_cursor_copilot_1_head_feat_api_v2_team_invite_link_endpoint_pr668

Conversation

@tomerqodo

Copy link
Copy Markdown

Benchmark PR from qodo-benchmark#668

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Invite creation lacks admin check 📘 Rule violation ⛨ Security
Description
• 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.
Code

apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[R31-39]

+  @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 };
Evidence
Compliance requires proper authentication/authorization for external inputs and sensitive actions.
The controller only requires a broad role (TEAM_MEMBER) and calls
TeamService.createInvite(teamId) without any user identity, while TeamService.createInvite
accepts only teamId (no userId), so it cannot enforce per-team admin authorization.

Rule 6: Generic: Security-First Input Validation and Data Handling
apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[31-40]
packages/features/ee/teams/services/teamService.ts[55-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. token returns identifier 📘 Rule violation ⛨ Security
Description
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.
Code

packages/features/ee/teams/services/teamService.ts[R87-91]

    const token = randomBytes(32).toString("hex");
-    await prisma.verificationToken.create({
+    const newToken = await prisma.verificationToken.create({
      data: {
        identifier: `invite-link-for-teamId-${teamId}`,
        token,
Evidence
The checklist requires identifiers to be self-documenting and data to be handled securely. The code
explicitly assigns the response field token to newToken.identifier (not the invite token), which
both violates meaningful naming and risks exposing predictable internal identifiers in a field that
callers will treat as a secret invitation token.

Rule 2: Generic: Meaningful Naming and Self-Documenting Code
Rule 6: Generic: Security-First Input Validation and Data Handling
packages/features/ee/teams/services/teamService.ts[87-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


3. Invite creation not audited 📘 Rule violation ⛨ Security
Description
• 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.
Code

apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[R1-40]

+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 };
+  }
Evidence
The audit trail requirement mandates logging critical actions with user identity and outcome. The
new controller creates invite links but contains no audit logging and does not use GetUser
(despite importing it), so the action is not attributable to a specific user in logs.

Rule 1: Generic: Comprehensive Audit Trails
apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[1-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View more (1)
4. Invite URL logic inverted 🐞 Bug ✓ Correctness
Description
• 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.
Code

packages/features/ee/teams/services/teamService.ts[R104-109]

  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);
Evidence
The web signup SSR logic treats org-context invite-by-link as cases where the
verificationToken.identifier is not a valid email. That flow only makes sense when users land on
/signup?token=.... Returning /teams?token=... for org-context invites likely bypasses this
intended org invite-by-link behavior.

packages/features/ee/teams/services/teamService.ts[104-112]
apps/web/lib/signup/getServerSideProps.tsx[164-183]
packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts[507-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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` =&gt; return `/teams?token=...`
 - else =&gt; return `/signup?token=...&amp;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



ⓘ The new review experience is currently in Beta. Learn more

Qodo Logo

Comment on lines +31 to +39
@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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

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

Comment on lines 87 to 91
const token = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
const newToken = await prisma.verificationToken.create({
data: {
identifier: `invite-link-for-teamId-${teamId}`,
token,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

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

Comment on lines +1 to +40
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 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

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

Comment on lines 104 to 109
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant