Skip to content

feat: api v2 team invite link endpoint#117

Open
tomerqodo wants to merge 2 commits into
qodo_claude_vs_qodo_base_feat_api_v2_team_invite_link_endpoint_pr6from
qodo_claude_vs_qodo_head_feat_api_v2_team_invite_link_endpoint_pr6
Open

feat: api v2 team invite link endpoint#117
tomerqodo wants to merge 2 commits into
qodo_claude_vs_qodo_base_feat_api_v2_team_invite_link_endpoint_pr6from
qodo_claude_vs_qodo_head_feat_api_v2_team_invite_link_endpoint_pr6

Conversation

@tomerqodo

Copy link
Copy Markdown

Benchmark PR from agentic-review-benchmarks#6

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add API v2 team invite link endpoint with role-based access

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Add team invite link endpoint to API v2 with role-based access control
• Implement TeamsInviteModule with controller and output DTOs
• Add comprehensive e2e tests for admin, member, and non-member scenarios
• Fix token generation logic to return identifier instead of token value
• Correct invite link building logic for organization context
Diagram
flowchart LR
  A["Client Request"] -->|POST /v2/teams/:teamId/invite| B["TeamsInviteController"]
  B -->|Validate Auth & Role| C["ApiAuthGuard + RolesGuard"]
  C -->|Create Invite| D["TeamService.createInvite"]
  D -->|Generate Token| E["Prisma VerificationToken"]
  D -->|Build Link| F["buildInviteLink"]
  F -->|Return| G["CreateInviteOutputDto"]
  G -->|Response| A
Loading

Grey Divider

File Changes

1. apps/api/v2/src/ee/platform-endpoints-module.ts ⚙️ Configuration changes +2/-0

Register TeamsInviteModule in platform endpoints

• Import new TeamsInviteModule for team invite functionality
• Register TeamsInviteModule in the platform endpoints module imports

apps/api/v2/src/ee/platform-endpoints-module.ts


2. apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts ✨ Enhancement +41/-0

Create team invite link endpoint controller

• Create POST endpoint /v2/teams/:teamId/invite for generating team invite links
• Implement role-based access control requiring TEAM_MEMBER role
• Apply ApiAuthGuard and RolesGuard for authentication and authorization
• Return CreateInviteOutputDto with status and invite data

apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts


3. apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts ✨ Enhancement +38/-0

Define invite output DTOs with API documentation

• Define InviteDataDto with token and inviteLink properties
• Create CreateInviteOutputDto wrapper with status and data fields
• Add comprehensive API documentation with examples for both fields
• Include validation decorators for enum and nested object validation

apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts


View more (3)
4. apps/api/v2/src/modules/teams/invite/teams-invite.module.ts ✨ Enhancement +11/-0

Create TeamsInviteModule with dependencies

• Create new NestJS module for team invite functionality
• Import PrismaModule, RedisModule, and MembershipsModule dependencies
• Register TeamsInviteController as the module controller

apps/api/v2/src/modules/teams/invite/teams-invite.module.ts


5. apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts 🧪 Tests +209/-0

Add comprehensive e2e tests for invite endpoint

• Add e2e tests for team admin creating invite links successfully
• Test that each invite request generates a new unique token
• Verify non-admin members receive 403 Forbidden response
• Verify non-team members receive 403 Forbidden response
• Use repository fixtures for user, team, and membership setup

apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts


6. packages/features/ee/teams/services/teamService.ts 🐞 Bug fix +4/-4

Fix token generation and invite link logic

• Fix token generation to return identifier instead of raw token value
• Correct invite link building logic to check isOrgContext properly
• Store created verification token in variable for proper return value
• Ensure invite links are correctly formatted for organization vs team context

packages/features/ee/teams/services/teamService.ts


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. InviteDataDto example lines too long 📘 Rule violation ✓ Correctness
Description
The added ApiProperty example strings exceed the 110 character line-width requirement, deviating
from the standardized Biome formatting configuration. This reduces formatting consistency and can
cause formatter/linter CI failures.
Code

apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts[R12-22]

+    example: "f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2",
+  })
+  token!: string;
+
+  @IsString()
+  @Expose()
+  @ApiProperty({
+    description:
+      "Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
+    example:
+      "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
Evidence
PR Compliance ID 3 requires 110 character line width. The new example strings in
invite.output.ts are single-line literals that exceed this limit.

AGENTS.md
apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts[9-23]

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 new Swagger `ApiProperty` `example` strings in `invite.output.ts` exceed the required 110 character line width, violating the Biome formatting standard.

## Issue Context
Formatting compliance requires 110 character max line width in TS/JS files.

## Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts[9-23]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. E2E test line too long 📘 Rule violation ✓ Correctness
Description
The added chained request(...).post(...).expect(...) statement is on a single line and exceeds the
110 character line-width formatting requirement. This breaks the standardized Biome formatting rules
and can fail format checks.
Code

apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts[80]

+      const first = await request(app.getHttpServer()).post(`/v2/teams/${team.id}/invite`).expect(200);
Evidence
PR Compliance ID 3 requires 110 character line width. The new line in the e2e spec keeps a long
chained call on one line, exceeding the configured width.

AGENTS.md
apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts[80-80]

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

## Issue description
A newly added test statement exceeds the 110 character line-width requirement.

## Issue Context
Formatting compliance requires Biome-standard formatting, including 110 character max line width.

## Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts[79-81]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Wrong invite token returned 🐞 Bug ✓ Correctness
Description
TeamService.createInvite returns the verificationToken.identifier string instead of the generated
verificationToken.token, so the API exposes a non-usable token that does not match the token
embedded in inviteLink. This breaks clients/tests expecting inviteLink to contain the returned
token and makes subsequent token-based operations fail.
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 service generates token and stores it in the verificationToken record, but returns
newToken.identifier (a constant per team) as the response token, while inviteLink is built
using the generated token. Both the existing org invite e2e and the new teams invite e2e assert
that inviteLink contains the returned token, which will fail with the current implementation.

packages/features/ee/teams/services/teamService.ts[87-101]
apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.e2e-spec.ts[89-98]
apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts[67-76]

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 `newToken.identifier` as the response `token`, but `inviteLink` is built using the generated `token`. This makes the returned token unusable and inconsistent.

### Issue Context
`identifier` is a deterministic string (e.g., `invite-link-for-teamId-<id>`), while the actual invite token is the random hex string stored in `verificationToken.token`.

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


View more (2)
4. Invite link logic reversed 🐞 Bug ✓ Correctness
Description
TeamService.buildInviteLink now returns a /teams?token=... link when isOrgContext is true and a
/signup?token=... link when false, inverting the intended behavior. This produces incorrect invite
URLs for org-context vs team-context invites and breaks onboarding/join flows.
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
isOrganizationOrATeamInOrganization is computed as true for org/teams-in-org, yet buildInviteLink
currently returns the team join link (/teams) for that case. Existing invite email generation uses
/signup?token=...&callbackUrl=... for signup/onboarding invites and /teams?token=... for team
join links, matching the pre-inversion behavior rather than the current one.

packages/features/ee/teams/services/teamService.ts[67-112]
packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts[508-516]
packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts[723-741]

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 an inverted `isOrgContext` check and returns the wrong URL shape for org-context invites vs normal team invites.

### Issue Context
`isOrgContext` is derived from `team.parentId || team.isOrganization` and is intended to signal organization-related invite flows.

### Fix Focus Areas
- packages/features/ee/teams/services/teamService.ts[104-111]
- packages/features/ee/teams/services/teamService.ts[67-68]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Invite creation privilege escalation 🐞 Bug ⛨ Security
Description
TeamsInviteController is annotated with @Roles("TEAM_MEMBER"), which permits regular members to
create invite links. This contradicts the intended admin-only behavior (tests expect 403 for
members) and diverges from the existing org-team invite endpoint which requires TEAM_ADMIN.
Code

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

+  @Post("/invite")
+  @Roles("TEAM_MEMBER")
+  @ApiOperation({ summary: "Create team invite link" })
Evidence
TEAM_MEMBER is the lowest entry in TEAM_ROLES and RolesGuard grants access when the caller’s role
index is <= required role index; therefore requiring TEAM_MEMBER allows MEMBER/ADMIN/OWNER. The
analogous org invite controller requires TEAM_ADMIN, and the new e2e spec explicitly expects 403 for
MEMBER.

apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[31-33]
apps/api/v2/src/lib/roles/constants.ts[9-13]
apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts[191-201]
apps/api/v2/src/modules/organizations/teams/invite/organizations-teams-invite.controller.ts[31-34]
apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts[148-150]

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

### Issue description
`TeamsInviteController` currently allows any `TEAM_MEMBER` to create invite links, enabling regular members to generate invites.

### Issue Context
Existing org team invite endpoints require `TEAM_ADMIN`, and the newly added e2e tests expect 403 for non-admin team members.

### Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[31-33]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

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

Grey Divider

Qodo Logo

Comment on lines +12 to +22
example: "f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2",
})
token!: string;

@IsString()
@Expose()
@ApiProperty({
description:
"Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
example:
"http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",

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. invitedatadto example lines too long 📘 Rule violation ✓ Correctness

The added ApiProperty example strings exceed the 110 character line-width requirement, deviating
from the standardized Biome formatting configuration. This reduces formatting consistency and can
cause formatter/linter CI failures.
Agent Prompt
## Issue description
The new Swagger `ApiProperty` `example` strings in `invite.output.ts` exceed the required 110 character line width, violating the Biome formatting standard.

## Issue Context
Formatting compliance requires 110 character max line width in TS/JS files.

## Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/outputs/invite.output.ts[9-23]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

});

it("should create a new invite on each request", async () => {
const first = await request(app.getHttpServer()).post(`/v2/teams/${team.id}/invite`).expect(200);

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. E2e test line too long 📘 Rule violation ✓ Correctness

The added chained request(...).post(...).expect(...) statement is on a single line and exceeds the
110 character line-width formatting requirement. This breaks the standardized Biome formatting rules
and can fail format checks.
Agent Prompt
## Issue description
A newly added test statement exceeds the 110 character line-width requirement.

## Issue Context
Formatting compliance requires Biome-standard formatting, including 110 character max line width.

## Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.e2e-spec.ts[79-81]

ⓘ 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

3. Wrong invite token returned 🐞 Bug ✓ Correctness

TeamService.createInvite returns the verificationToken.identifier string instead of the generated
verificationToken.token, so the API exposes a non-usable token that does not match the token
embedded in inviteLink. This breaks clients/tests expecting inviteLink to contain the returned
token and makes subsequent token-based operations fail.
Agent Prompt
### Issue description
`TeamService.createInvite()` returns `newToken.identifier` as the response `token`, but `inviteLink` is built using the generated `token`. This makes the returned token unusable and inconsistent.

### Issue Context
`identifier` is a deterministic string (e.g., `invite-link-for-teamId-<id>`), while the actual invite token is the random hex string stored in `verificationToken.token`.

### 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 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 link logic reversed 🐞 Bug ✓ Correctness

TeamService.buildInviteLink now returns a /teams?token=... link when isOrgContext is true and a
/signup?token=... link when false, inverting the intended behavior. This produces incorrect invite
URLs for org-context vs team-context invites and breaks onboarding/join flows.
Agent Prompt
### Issue description
`buildInviteLink()` uses an inverted `isOrgContext` check and returns the wrong URL shape for org-context invites vs normal team invites.

### Issue Context
`isOrgContext` is derived from `team.parentId || team.isOrganization` and is intended to signal organization-related invite flows.

### Fix Focus Areas
- packages/features/ee/teams/services/teamService.ts[104-111]
- packages/features/ee/teams/services/teamService.ts[67-68]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +31 to +33
@Post("/invite")
@Roles("TEAM_MEMBER")
@ApiOperation({ summary: "Create team invite link" })

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

5. Invite creation privilege escalation 🐞 Bug ⛨ Security

TeamsInviteController is annotated with @Roles("TEAM_MEMBER"), which permits regular members to
create invite links. This contradicts the intended admin-only behavior (tests expect 403 for
members) and diverges from the existing org-team invite endpoint which requires TEAM_ADMIN.
Agent Prompt
### Issue description
`TeamsInviteController` currently allows any `TEAM_MEMBER` to create invite links, enabling regular members to generate invites.

### Issue Context
Existing org team invite endpoints require `TEAM_ADMIN`, and the newly added e2e tests expect 403 for non-admin team members.

### Fix Focus Areas
- apps/api/v2/src/modules/teams/invite/controllers/teams-invite.controller.ts[31-33]

ⓘ 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