Skip to content

Bootsecurity dev#1022

Closed
BilalG1 wants to merge 19 commits intodevfrom
bootsecurity-dev
Closed

Bootsecurity dev#1022
BilalG1 wants to merge 19 commits intodevfrom
bootsecurity-dev

Conversation

@BilalG1
Copy link
Contributor

@BilalG1 BilalG1 commented Nov 19, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Invite team members with specific roles and permissions during invitation
    • View available team roles and permissions when managing invitations
    • See assigned roles displayed on team member profiles and lists
  • Documentation

    • Updated SDK types and Python templates to reflect new role management capabilities
    • Added examples for permission-based team invitations
  • Tests

    • Added extensive end-to-end tests covering role-permission fetching, invitation sending, and acceptance flows

✏️ Tip: You can customize this high-level summary in your review settings.


Note

Adds role-based permissions to team invitations (applied on accept), exposes permission_ids in invitations and team member profiles, and introduces an endpoint to list available role permissions, with SDK/UI/docs updates.

  • Backend/API:
    • Team invitations now carry optional permission_ids; validated server-side and applied on accept (grantTeamPermission).
    • New endpoint GET /api/v1/team-invitations/role-permissions to list team permission definitions.
    • Invitation listing returns permission_ids; acceptance enforces limits and assigns roles.
    • Team member profiles now include permission_ids (batch-fetched to avoid N+1).
  • SDK/Types:
    • Client/Server interfaces support permissionIds on invite and expose permission_ids in invitations and team profiles.
    • Project exposes listTeamPermissionDefinitions().
    • Updated Team, TeamInvitation, TeamProfile typings accordingly.
  • UI (template):
    • Invitations list and member list display role badges derived from permissionIds.
  • Docs:
    • Python and SDK docs updated for sending invites with roles, listing role permissions, and reading permission_ids.
  • Tests:
    • E2E coverage for role-permissions endpoint, invitation permission_ids flows, application on accept, validation errors, and profile permission_ids.

Written by Cursor Bugbot for commit d697810. This will update automatically on new commits. Configure here.

bootssecurity and others added 16 commits August 31, 2025 00:10
- Add permission_ids field to team member profile schemas (client/server)
- Update team member profiles API to include permission_ids in responses
- Modify team member list UI to display roles based on permission_ids
- Update team invitation endpoints to handle permission_ids
- Add role-permissions endpoint for fetching available permissions
- Update documentation with permission_ids examples and type definitions
- Add conceptual documentation for role-based team invitations
…s API

- Add permission_ids field to team-member-profiles schema (client & server)
- Update CRUD operations to fetch and include direct permission_ids
- Fix team-member-list-section to use permission_ids for role display
- Optimize permission fetching to avoid N+1 queries
- Add atomic permission granting in team-invitations/accept
- Add e2e test coverage for permission_ids in API responses
- Fix naming convention violations (snake_case to camelCase)
- Remove debug console.log statements

Resolves broken member list roles functionality
- Changed the return statement in the `get` method to use `await res.json()` instead of `res.data` for proper JSON handling.

This change ensures that the API response is correctly parsed as JSON, improving data handling in the client interface.
…n_ids validation

- Added tests for fetching role permissions from the role-permissions endpoint.
- Implemented tests for sending invitations with specific permission_ids and verifying their application upon acceptance.
- Updated team member profile tests to ensure permission_ids are included in responses for both the inviting and invited users.
- Refactored permission fetching logic in the frontend to utilize the new project.listTeamPermissionDefinitions method.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Introduced tests for handling invalid, malformed, and mixed permission_ids in team invitations, ensuring proper error responses.
- Added tests for permission escalation scenarios, verifying that users without invite permissions cannot grant elevated permissions.
- Implemented tests for backwards compatibility with empty and omitted permission_ids.
- Included server access tests to confirm that invitations can be sent with permission_ids bypassing user permissions.
- Enhanced role-permissions endpoint tests for consistent responses between client and server access.
- Updated logic to ensure that only non-default roles are included in permissionIds when inviting users to a team, enhancing the accuracy of permission assignments during the invitation process.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Updated test cases to use `receiveMailbox.emailAddress` for email field in team invitation requests.
- Simplified error response assertions by removing unnecessary nested property checks for 'error', focusing directly on 'code' and 'message' properties.
- Enhanced consistency in error handling across various invitation scenarios, ensuring clearer validation of permission-related errors.
@vercel
Copy link

vercel bot commented Nov 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 20, 2025 11:41am
stack-dashboard Ready Ready Preview Comment Nov 20, 2025 11:41am
stack-demo Ready Ready Preview Comment Nov 20, 2025 11:41am
stack-docs Ready Ready Preview Comment Nov 20, 2025 11:41am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds permission_ids support across team invitations and member profiles: new role-permissions endpoint, permission_ids accepted on send-code, preserved on invitation records, applied when accepting invitations, propagated through CRUD/SDK/server layers, plus UI/docs/tests updates and permission validation/guards.

Changes

Cohort / File(s) Change Summary
Backend: Invitation acceptance
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
Adds optional permission_ids to verification code schema; deduplicates and grants each permission via grantTeamPermission inside the acceptance transaction after creating membership.
Backend: Invitation CRUD & send
apps/backend/src/app/api/latest/team-invitations/crud.tsx, apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx
crud now surfaces permission_ids from code data (fallback []). send-code accepts optional permission_ids, enforces server-only usage (throws Forbidden for client auth), validates IDs against config, and passes them to sendCode.
Backend: Role permissions route
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
New GET route returning team-scoped permission definitions (items, is_paginated: false) via listPermissionDefinitions.
Backend: Team member permissions CRUD
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
Adds fetchTeamMemberPermissions helper; extends prismaToCrud to accept permissionIds and include permission_ids in CRUD responses for list/read/update flows.
SDK: Shared interfaces & schemas
packages/stack-shared/src/interface/client-interface.ts, packages/stack-shared/src/interface/server-interface.ts, packages/stack-shared/src/interface/crud/team-invitation.ts, packages/stack-shared/src/interface/crud/team-member-profiles.ts
Adds getTeamRolePermissions() client method. Adds permission_ids to client read schemas for invitations and member profiles. sendServerTeamInvitation gains optional permissionIds and forwards them as permission_ids.
SDK: App implementations & types
packages/template/src/lib/stack-app/.../client-app-impl.ts, .../server-app-impl.ts, .../admin-app-impl.ts, packages/template/src/lib/stack-app/teams/index.ts, .../projects/index.ts
Propagates permissionIds through client/server CRUD mappings and returned types; adds listTeamPermissionDefinitions() on admin/client app layers; inviteUser signatures now accept optional permissionIds.
UI: Templates / components
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx, .../team-member-list-section.tsx
Adds a Role column and Badge display driven by permissionIds; computes role display mapping (team_admin → Admin, team_member → Member) and adjusts layout.
Docs: Python & SDK docs
docs/templates-python/concepts/teams-management.mdx, docs/templates/sdk/types/team-profile.mdx, docs/templates/sdk/types/team.mdx
Adds get_team_role_permissions() example; send_team_invitation(...) accepts permission_ids optional; documents permissionIds on TeamProfile and updated invite/list signatures.
Tests: E2E backend
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts, apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
Adds comprehensive tests for role-permissions endpoint, permission_ids validation/edge-cases (404/400/403/null/mixed), server-vs-client guards, permission application on accept, and member-profile permission_ids propagation.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI/Client
    participant SDK as SDK
    participant API as Backend
    participant DB as Database

    UI->>SDK: GET listTeamPermissionDefinitions()
    SDK->>API: GET /team-invitations/role-permissions
    API->>DB: Query permission definitions (scope: team)
    DB-->>API: Permission definitions
    API-->>SDK: { items: [...], is_paginated: false }
    SDK-->>UI: available roles shown

    UI->>SDK: inviteUser(email, callbackUrl, permissionIds)
    SDK->>API: POST /team-invitations/send-code { permission_ids }
    API->>API: Validate permission_ids (server-only guard)
    API->>DB: Create invitation code with permission_ids
    DB-->>API: Stored code
    API-->>SDK: Invitation created
Loading
sequenceDiagram
    participant Inviter as Inviter (client/server)
    participant CodeHandler as Send-Code Handler
    participant Accepter as Accept Handler
    participant PermSvc as Permission Granting
    participant DB as Database

    Inviter->>CodeHandler: POST send-code (email, permission_ids?)
    CodeHandler->>DB: Store code.data.permission_ids
    Note right of DB: invitation code saved with permission_ids

    Accepter->>CodeHandler: POST accept (code)
    CodeHandler->>DB: Create team membership (within tx)
    CodeHandler->>PermSvc: For each deduped permission_id -> grantTeamPermission (within same tx)
    PermSvc-->>DB: Insert grants
    DB-->>Accepter: Membership + permissions persisted
    Accepter-->>Inviter: Acceptance complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas to focus on:

  • Permission validation and server/client guard logic in send-code/route.tsx.
  • Transactional grant logic and deduplication in verification-code-handler.tsx.
  • Batch permission fetch correctness and mapping in team-member-profiles/crud.tsx.
  • SDK/CRUD data mappings to ensure permissionIds consistently flow through client/server types.
  • E2E tests for edge cases and backward compatibility.

Possibly related PRs

Poem

🐰 I hopped through code with gentle paws,

invited roles, checked every clause.
Permissions trimmed and nicely set,
badges bloom where members met.
Hooray — the team's now neatly blessed! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Bootsecurity dev' is vague and does not clearly convey the main changes in this comprehensive pull request about role-based permissions for team invitations. Use a more descriptive title such as 'Add role-based permissions to team invitations' to clearly summarize the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description includes only an HTML comment referencing CONTRIBUTING.md guidelines, but the Cursor-generated summary provides comprehensive context about the actual changes (role-based permissions for team invitations, new endpoint, SDK/UI/docs updates).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bootsecurity-dev

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75f1416 and d697810.

📒 Files selected for processing (2)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1 hunks)
  • packages/template/src/lib/stack-app/projects/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/projects/index.ts
⏰ 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). (12)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
  • GitHub Check: restart-dev-and-test
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: check_prisma_migrations (22.x)

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 19, 2025

Greptile Summary

  • Added role-based team invitations with permission_ids support across backend APIs, team member profiles, and UI components
  • Implemented security fix preventing client-side requests from setting permission_ids (server-only feature)
  • Multiple E2E tests expect client-side permission_ids to work but will fail due to security restriction on line 41-43 of send-code/route.tsx

Confidence Score: 2/5

  • Critical test failures will occur due to conflicting security fix and test expectations
  • The latest commit added proper security to block client-side permission_ids, but 7+ tests still expect client requests with permission_ids to succeed, causing guaranteed test failures
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts needs immediate updates to align with security fix

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx Added security check to prevent client-side requests from setting permission_ids, but test on line 576 expects client-side request with permission_ids to return 200 instead of 403
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts Added comprehensive tests for permission_ids feature, but test on line 576 conflicts with security fix that blocks client-side permission_ids
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx Added permission granting logic when accepting invitation with proper deduplication and validation through grantTeamPermission

Sequence Diagram

sequenceDiagram
    participant User
    participant Client
    participant API as Team Invitations API
    participant Handler as Verification Handler
    participant DB as Database
    participant Email as Email Service

    User->>Client: Send invitation with role
    Client->>API: POST /team-invitations/send-code (server-side)
    API->>API: Validate permission_ids
    API->>Handler: Create verification code
    Handler->>DB: Store invitation with permission_ids
    Handler->>Email: Send invitation email
    Email-->>User: Invitation email with link
    
    User->>Client: Click invitation link
    Client->>Handler: POST /accept (with code)
    Handler->>DB: Verify invitation code
    Handler->>DB: Create team membership
    Handler->>DB: Grant permissions from permission_ids
    Handler-->>Client: Success response
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

22 files reviewed, 8 comments

Edit Code Review Agent Settings | Greptile
React with 👍 or 👎 to share your feedback on this new summary format

if (response.body.items.length > 0) {
const item = response.body.items[0];
expect(item).toHaveProperty('id');
expect(item).toHaveProperty('contained_permission_ids');
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: test will fail - the security fix on line 41-43 of apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx blocks client-side requests from setting permission_ids, but this test expects status 200 instead of 403

Suggested change
expect(item).toHaveProperty('contained_permission_ids');
expect(sendResponse.status).toBe(403);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 576:576

Comment:
**logic:** test will fail - the security fix on line 41-43 of `apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx` blocks client-side requests from setting `permission_ids`, but this test expects status 200 instead of 403

```suggestion
  expect(sendResponse.status).toBe(403);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +586 to +591
// Grant invite permission
await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$invite_members`, {
accessType: "server",
method: "POST",
body: {},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: these assertions will never be reached since the test now expects a 403 error from the client-side security check - remove or update these assertions

Suggested change
// Grant invite permission
await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$invite_members`, {
accessType: "server",
method: "POST",
body: {},
});
// Verify that permission_ids cannot be set from client-side
expect(sendResponse.body).toHaveProperty('code');
expect(sendResponse.body.message).toMatch(/permission_ids.*server-side/i);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 586:591

Comment:
**logic:** these assertions will never be reached since the test now expects a 403 error from the client-side security check - remove or update these assertions

```suggestion
  // Verify that permission_ids cannot be set from client-side
  expect(sendResponse.body).toHaveProperty('code');
  expect(sendResponse.body.message).toMatch(/permission_ids.*server-side/i);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 594 to 629

const sendResponse = await niceBackendFetch("/api/v1/team-invitations/send-code", {
method: "POST",
accessType: "client",
body: {
email: receiveMailbox.emailAddress,
team_id: teamId,
callback_url: "http://localhost:12345/some-callback-url",
permission_ids: ["team_admin"],
},
});

expect(sendResponse.status).toBe(200);

// Verify invitation was created with permission_ids
const listResponse = await niceBackendFetch(`/api/v1/team-invitations?team_id=${teamId}`, {
accessType: "server",
method: "GET",
});

expect(listResponse.status).toBe(200);
expect(listResponse.body.items.length).toBeGreaterThan(0);
expect(listResponse.body.items[0]).toHaveProperty('permission_ids');
expect(listResponse.body.items[0].permission_ids).toEqual(["team_admin"]);
});

it("applies permission_ids when accepting invitation", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const { userId } = await Auth.Otp.signIn();
const { teamId } = await Team.create();

// Grant invite permission
await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$invite_members`, {
accessType: "server",
method: "POST",
body: {},
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: test logic is broken - client-side requests with permission_ids are now blocked by the security fix, so this test will fail at line 609 with a 403 error before reaching the invitation acceptance logic

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 594:629

Comment:
**logic:** test logic is broken - client-side requests with `permission_ids` are now blocked by the security fix, so this test will fail at line 609 with a 403 error before reaching the invitation acceptance logic

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 634 to 656
// Send invitation with permission_ids
await niceBackendFetch("/api/v1/team-invitations/send-code", {
method: "POST",
accessType: "client",
body: {
email: receiveMailbox.emailAddress,
team_id: teamId,
callback_url: "http://localhost:12345/some-callback-url",
permission_ids: ["team_admin"],
},
});

// Accept invitation as new user
backendContext.set({ mailbox: receiveMailbox });
await Auth.Otp.signIn();
await Team.acceptInvitation();

// Verify user received the permission
const newUserId = (await User.getCurrent()).id;
const permissionResponse = await niceBackendFetch(
`/api/v1/team-permissions/${teamId}/${newUserId}/team_admin`,
{
accessType: "server",
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: test uses client-side accessType with permission_ids, which is now blocked - change to server-side access or expect 403 error

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 634:656

Comment:
**logic:** test uses client-side `accessType` with `permission_ids`, which is now blocked - change to server-side access or expect 403 error

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 658 to 678
}
);

expect(permissionResponse.status).toBe(200);
expect(permissionResponse.body).toHaveProperty('id', 'team_admin');
});

// Edge case tests for invalid permission_ids
it("returns 400 error for invitation with non-existent permission_ids", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const { userId } = await Auth.Otp.signIn();
const { teamId } = await Team.create();

// Grant invite permission
await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$invite_members`, {
accessType: "server",
method: "POST",
body: {},
});

const receiveMailbox = createMailbox();
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: test uses client-side accessType with permission_ids (malformed as number), but will now fail with 403 before schema validation - change to server-side or adjust expectations

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 658:678

Comment:
**logic:** test uses client-side `accessType` with `permission_ids` (malformed as number), but will now fail with 403 before schema validation - change to server-side or adjust expectations

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 680 to 701
const response = await niceBackendFetch("/api/v1/team-invitations/send-code", {
method: "POST",
accessType: "client",
body: {
email: receiveMailbox.emailAddress,
team_id: teamId,
callback_url: "http://localhost:12345/some-callback-url",
permission_ids: ["non_existent_role"],
},
});

expect(response.status).toBe(400);
expect(response.body).toHaveProperty('code');
expect(response.body).toHaveProperty('message');
expect(response.body.message).toMatch(/invalid permission|permission.*not found/i);
});

it("returns 400 error for invitation with malformed permission_ids", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const { userId } = await Auth.Otp.signIn();
const { teamId } = await Team.create();

Copy link
Contributor

Choose a reason for hiding this comment

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

logic: same issue - client-side request with permission_ids will return 403 before validation

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 680:701

Comment:
**logic:** same issue - client-side request with `permission_ids` will return 403 before validation

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 730 to 750
const { teamId } = await Team.create();

// Grant invite permission
await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$invite_members`, {
accessType: "server",
method: "POST",
body: {},
});

const receiveMailbox = createMailbox();

const response = await niceBackendFetch("/api/v1/team-invitations/send-code", {
method: "POST",
accessType: "client",
body: {
email: receiveMailbox.emailAddress,
team_id: teamId,
callback_url: "http://localhost:12345/some-callback-url",
permission_ids: ["team_admin", "invalid_permission"],
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: client-side request with empty permission_ids array will fail with 403 - the security check blocks ANY permission_ids value (including empty arrays) from client-side

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 730:750

Comment:
**logic:** client-side request with empty `permission_ids` array will fail with 403 - the security check blocks ANY `permission_ids` value (including empty arrays) from client-side

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 810 to 830
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const { userId } = await Auth.Otp.signIn();
const { teamId } = await Team.create();

// Grant invite permission
await niceBackendFetch(`/api/v1/team-permissions/${teamId}/${userId}/$invite_members`, {
accessType: "server",
method: "POST",
body: {},
});

const receiveMailbox = createMailbox();

const response = await niceBackendFetch("/api/v1/team-invitations/send-code", {
method: "POST",
accessType: "client",
body: {
email: receiveMailbox.emailAddress,
team_id: teamId,
callback_url: "http://localhost:12345/some-callback-url",
permission_ids: [], // Empty array
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: client-side request with permission_ids: null will return 403 before schema validation runs

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Line: 810:830

Comment:
**logic:** client-side request with `permission_ids: null` will return 403 before schema validation runs

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs/templates/sdk/types/team.mdx (1)

212-237: Align Team invitation docs with new permissionIds field

The listInvitations() “Returns” text still shows Promise<{ id: string, email: string, expiresAt: Date }[]>, but the signature and examples now include permissionIds: string[]. Also, the inviteUser parameter docs don’t mention the new optional permissionIds?: string[] field even though the signature and examples use it. Please update the “Returns” description and add a permissionIds property to the inviteUser parameter accordion so the docs match the actual API.

Also applies to: 324-339, 357-371

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

919-928: Client Team.inviteUser currently ignores permissionIds

You’ve correctly plumbed permissionIds from CRUD into:

  • _clientTeamUserFromCrudteamProfile.permissionIds
  • _clientTeamInvitationFromCrudpermissionIds
  • _editableTeamProfileFromCrudpermissionIds

and the Team type now exposes inviteUser(options: { email, callbackUrl?: string, permissionIds?: string[] }). However, the concrete implementation returned by _clientTeamFromCrud still has:

async inviteUser(options: { email: string, callbackUrl?: string }) {
  await app._interface.sendTeamInvitation({
    teamId: crud.id,
    email: options.email,
    session,
    callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
  });
  await app._teamInvitationsCache.refresh([session, crud.id]);
}

so any permissionIds passed by callers are silently dropped and never reach the backend.

Please update this implementation to accept and forward permissionIds so the public API actually supports role-based invitations, e.g.:

-      async inviteUser(options: { email: string, callbackUrl?: string }) {
+      async inviteUser(options: { email: string, callbackUrl?: string, permissionIds?: string[] }) {
         await app._interface.sendTeamInvitation({
           teamId: crud.id,
           email: options.email,
-          session,
-          callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
+          session,
+          callbackUrl: options.callbackUrl ?? constructRedirectUrl(app.urls.teamInvitation, "callbackUrl"),
+          permissionIds: options.permissionIds,
         });
         await app._teamInvitationsCache.refresh([session, crud.id]);
       },

Also applies to: 930-941, 993-1010, 1192-1209

apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)

191-216: Fix user ID used for last-active lookup in onUpdate

In onUpdate, the call to getUserLastActiveAtMillis uses:

return prismaToCrud(
  db,
  await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUser.projectUserId) ?? db.projectUser.createdAt.getTime(),
  perms.map(p => p.permissionId),
);

db is a TeamMember with a projectUserId field; the nested db.projectUser is the related record, which is very unlikely to have a projectUserId property. This will either fail type-checking or pass undefined as the user ID, breaking the last-active computation.

It should mirror the other call sites that use db.projectUserId as the user identifier, e.g.:

-      return prismaToCrud(db, await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUser.projectUserId) ?? db.projectUser.createdAt.getTime(), perms.map(p => p.permissionId));
+      const lastActiveAt =
+        (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId))
+        ?? db.projectUser.createdAt.getTime();
+      return prismaToCrud(db, lastActiveAt, perms.map((p) => p.permissionId));
🧹 Nitpick comments (3)
packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (1)

38-45: Consider handling multiple role permissions.

The current logic only uses the first non-system permission (filteredPermissions[0]). If a user has multiple role-based permissions assigned, only the first one will be displayed.

Consider whether this is the intended behavior or if you should:

  • Display all roles (e.g., "Admin, Manager")
  • Prioritize certain roles over others
  • Display the "highest" permission level
packages/stack-shared/src/interface/crud/team-member-profiles.ts (1)

12-12: Consider using permissionDefinitionIdSchema for consistency.

The team-invitation.ts file uses permissionDefinitionIdSchema for its permission_ids field, while this file uses yupString(). For consistency and better type safety, consider using the same schema type:

-  permission_ids: schemaFields.yupArray(schemaFields.yupString().defined()).defined(),
+  permission_ids: schemaFields.yupArray(schemaFields.permissionDefinitionIdSchema.defined()).defined(),

This ensures consistent validation rules across all permission ID fields.

apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)

15-37: Permission fetching helper is good; consider reusing it and tightening types

The new fetchTeamMemberPermissions helper and its use in onList and onRead nicely avoid an N+1 pattern and keep permission aggregation in one place. In onUpdate you reimplement a narrower query against teamMemberDirectPermission; for consistency and maintainability you might want to reuse the helper there as well (passing a single projectUserId) instead of duplicating query logic.

Also, the helper currently types tx as any. If possible, tightening this to the appropriate Prisma transaction client type would catch more mistakes at compile time, especially as this query evolves.

Also applies to: 105-121, 160-172, 206-215

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0335e04 and 38dfd88.

📒 Files selected for processing (22)
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (3 hunks)
  • apps/backend/src/app/api/latest/team-invitations/crud.tsx (1 hunks)
  • apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (1 hunks)
  • apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (4 hunks)
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (4 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (2 hunks)
  • docs/concepts/role-based-team-invitations.mdx (1 hunks)
  • docs/templates-python/concepts/teams-management.mdx (8 hunks)
  • docs/templates/sdk/types/team-profile.mdx (2 hunks)
  • docs/templates/sdk/types/team.mdx (4 hunks)
  • packages/stack-shared/src/interface/client-interface.ts (1 hunks)
  • packages/stack-shared/src/interface/crud/team-invitation.ts (1 hunks)
  • packages/stack-shared/src/interface/crud/team-member-profiles.ts (1 hunks)
  • packages/stack-shared/src/interface/server-interface.ts (2 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (3 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (3 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (4 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (4 hunks)
  • packages/template/src/lib/stack-app/projects/index.ts (1 hunks)
  • packages/template/src/lib/stack-app/teams/index.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
🧬 Code graph analysis (8)
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (3)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
  • createSmartRouteHandler (209-294)
packages/stack-shared/src/schema-fields.ts (7)
  • yupObject (247-251)
  • clientOrHigherAuthTypeSchema (497-497)
  • adaptSchema (330-330)
  • yupNumber (191-194)
  • yupString (187-190)
  • yupArray (213-216)
  • yupBoolean (195-198)
apps/backend/src/lib/permissions.tsx (1)
  • listPermissionDefinitions (183-193)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (4)
apps/e2e/tests/helpers.ts (1)
  • it (12-12)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (4)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (729-738)
apps/backend/src/prisma-client.tsx (1)
  • retryTransaction (151-226)
apps/backend/src/app/api/latest/team-memberships/crud.tsx (1)
  • teamMembershipsCrudHandlers (43-160)
apps/backend/src/lib/permissions.tsx (1)
  • grantTeamPermission (96-138)
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
packages/stack-shared/src/interface/client-interface.ts (1)
packages/stack-shared/src/sessions.ts (1)
  • InternalSession (71-241)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
apps/backend/src/app/api/latest/users/crud.tsx (2)
  • getUsersLastActiveAtMillis (220-243)
  • getUserLastActiveAtMillis (188-194)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
  • constructRedirectUrl (4-20)
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (2)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (729-738)
packages/stack-shared/src/utils/errors.tsx (1)
  • StatusError (152-261)
⏰ 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). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: restart-dev-and-test
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
🔇 Additional comments (19)
packages/stack-shared/src/interface/crud/team-invitation.ts (1)

10-10: LGTM! Schema definition is correct.

The permission_ids field is properly defined as a required array of permission definition IDs, aligning with the role-based invitation feature.

packages/template/src/lib/stack-app/projects/index.ts (1)

14-14: LGTM! Method signature is well-defined.

The listTeamPermissionDefinitions() method provides a clean API for fetching team permission definitions with appropriate typing.

apps/backend/src/app/api/latest/team-invitations/crud.tsx (1)

56-56: LGTM! Proper fallback handling.

The fallback to an empty array ensures that permission_ids always returns a defined array, maintaining consistency with the client schema definition.

apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)

311-372: LGTM! Comprehensive test coverage.

The new tests effectively validate:

  • Presence of permission_ids field in team member profiles
  • Correct propagation of granted permissions to the field

The test flow (create team → invite → accept → grant permission → verify) provides solid end-to-end validation.

packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)

174-181: LGTM! Clean implementation.

The method properly delegates to the interface layer and maps the response to the expected public API shape.

packages/stack-shared/src/interface/client-interface.ts (1)

727-736: LGTM!

The implementation is clean and follows existing patterns in the codebase. The method correctly fetches role permissions and returns a well-typed response.

packages/stack-shared/src/interface/server-interface.ts (1)

657-679: LGTM!

The optional permissionIds parameter is correctly added and properly propagated to the request body. This is a non-breaking change that extends the invitation functionality.

packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)

32-43: Simple role mapping is appropriate for current use case.

The getRoleDisplayName function uses a straightforward approach to determine role display names. It filters system permissions and maps known IDs to friendly names, which is suitable for the current invitation display context.


45-86: LGTM!

The table structure changes are consistent and properly implemented. The new "Role" column is correctly added, and the colspan is appropriately updated to match the new column count.

docs/templates/sdk/types/team-profile.mdx (1)

54-66: LGTM!

The documentation for permissionIds is clear, well-structured, and follows the established documentation patterns in the file.

apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)

119-130: Good deduplication of permission IDs.

The code correctly deduplicates permission IDs before granting them, which is appropriate defensive programming to handle potential duplicates in the input.

docs/templates-python/concepts/teams-management.mdx (3)

220-276: LGTM!

The documentation for send_team_invitation is comprehensive and includes clear examples for different role scenarios (admin, member, and default). The optional permission_ids parameter is well-documented and properly handled.


298-322: LGTM!

The documentation for get_team_role_permissions is clear and provides a useful example of how to retrieve and display available permissions.


195-211: LGTM!

The documentation updates consistently integrate permission IDs throughout the teams management examples. The role determination logic is clear and the examples are helpful for understanding how to work with team permissions.

Also applies to: 343-348, 456-488

apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)

36-76: Good security measure to prevent client-side permission assignment.

The guard at lines 41-43 correctly prevents client-side requests from setting permission_ids, which is an important security control. Only server-side requests (with proper authentication) can assign specific permissions during invitations.

apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1)

559-992: Strong coverage for permission_ids & role-permissions, but verify membership and error shapes

The new tests do a good job exercising role-permissions, permission_ids validation, escalation, and backwards-compat scenarios end to end. Two things to double-check:

  • Several tests ("can send invitation with permission_ids", "applies permission_ids when accepting invitation", and some error cases) call Team.create() and then grant $invite_members via /api/v1/team-permissions/... without explicitly adding the inviter as a team member (unlike earlier tests that use createAndAddCurrentUserWithoutMemberPermission()). If team permissions are meant to apply only to members, consider either using that helper or confirming that Team.create() always enrolls the creator as a member.

  • New negative-path tests assert response.body.message (and match regexes against it), whereas earlier errors in this file typically exposed code/details/error with x-stack-known-error headers. Please confirm that the updated endpoints actually return a message field in these cases; otherwise it may be safer to assert against error or a well-defined code.

packages/template/src/lib/stack-app/teams/index.ts (1)

10-14: Type surface for permissionIds on team members & invitations looks consistent

Adding permissionIds to TeamMemberProfile, optional permissionIds to TeamInvitation, and extending both Team.inviteUser and ServerTeam.inviteUser to accept permissionIds?: string[] aligns with the surrounding client/server implementations and tests. The new types are coherent and should make the permissions data available where expected.

Also applies to: 30-36, 38-52, 86-96

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

888-910: Confirm getTeamRolePermissions interface contract for listTeamPermissionDefinitions

_clientProjectFromCrud wires Project.listTeamPermissionDefinitions to:

const session = await this._getSession();
const result = await this._interface.getTeamRolePermissions({ session });
return result.items;

This assumes getTeamRolePermissions takes a single options object containing session and returns an object with an items array (and likely is_paginated). Please confirm this matches the actual StackClientInterface method signature and return shape; if the interface instead follows the usual (options, session) pattern, you may need to pass session as a separate argument.

packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)

273-291: Server-side permissionIds wiring and inviteUser updates look correct

The server app now:

  • Exposes permissionIds on editable team profiles, server team users, and team invitations.
  • Extends ServerTeam.inviteUser to accept permissionIds?: string[] and forwards them to _interface.sendServerTeamInvitation, then refreshes the invitations cache.

This keeps the server-side API in sync with the shared types and backend CRUD and should support role-based invitations as intended.

Also applies to: 772-781, 786-797, 853-861

Comment on lines 111 to 131
await retryTransaction(prisma, async (tx) => {
await teamMembershipsCrudHandlers.adminCreate({
tenancy,
team_id: data.team_id,
user_id: user.id,
data: {},
});

// Apply additional specific permissions if provided (with deduplication)
if (data.permission_ids && data.permission_ids.length > 0) {
const uniquePermissionIds = [...new Set(data.permission_ids)];
for (const permissionId of uniquePermissionIds) {
await grantTeamPermission(tx, {
tenancy,
teamId: data.team_id,
userId: user.id,
permissionId,
});
}
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify transaction safety for adminCreate.

The implementation correctly wraps membership creation and permission granting in a transaction for atomicity. However, calling adminCreate inside retryTransaction may have side effects (e.g., webhooks) that could be triggered multiple times if the transaction is retried.

Consider either:

  1. Verifying that adminCreate only performs database operations within the transaction
  2. Using lower-level functions that don't trigger side effects, similar to how addUserToTeam is used in team-memberships/crud.tsx

Run the following to check how adminCreate is implemented and whether it triggers webhooks:


🏁 Script executed:

#!/bin/bash
# Check the implementation of adminCreate in team memberships CRUD
ast-grep --pattern $'adminCreate: async ({ $$$, data }) => {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for adminCreate function definition more broadly
rg -nP 'adminCreate\s*:\s*async|function\s+adminCreate' --type=ts --type=tsx -A5

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find team memberships CRUD file and search for adminCreate there
fd -e tsx -e ts -path '*team*membership*crud*' | head -20

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Search for where adminCreate is exported or defined
rg -nP 'export.*adminCreate|const\s+adminCreate|adminCreate\s*=' --type=ts --type=tsx -B2 -A10

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for adminCreate definition across the codebase
rg -n 'adminCreate.*async.*{' -A15 | head -50

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Find files in team-memberships directory
fd 'crud' apps/backend/src

Length of output: 1692


🏁 Script executed:

#!/bin/bash
# Search for adminCreate and webhook-related patterns
rg -n -i 'adminCreate|webhook' apps/backend/src/app/api --max-count=100

Length of output: 6174


🏁 Script executed:

#!/bin/bash
# Read the team-memberships/crud.tsx file to see adminCreate implementation
cat apps/backend/src/app/api/latest/team-memberships/crud.tsx

Length of output: 4992


🏁 Script executed:

#!/bin/bash
# Find the crud-handler file to understand how adminCreate is generated
cat apps/backend/src/route-handlers/crud-handler.tsx | head -200

Length of output: 9264


🏁 Script executed:

#!/bin/bash
# Search for how adminCreate is used elsewhere with retryTransaction
rg -n 'retryTransaction.*adminCreate|adminCreate.*retryTransaction' apps/backend/src -B3 -A3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any version of adminCreate that accepts transaction parameter
rg -n 'adminCreate.*tx\|adminCreate.*transaction' apps/backend/src -B2 -A5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the verification-code-handler full context around the adminCreate call
cat apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx

Length of output: 5151


🏁 Script executed:

#!/bin/bash
# Search for patterns where addUserToTeam is used directly (lower-level function)
rg -n 'addUserToTeam' apps/backend/src -B2 -A2

Length of output: 2291


Replace adminCreate call with direct addUserToTeam to avoid nested transactions and repeated webhook triggers.

Calling adminCreate inside retryTransaction creates a nested transaction problem. The adminCreate handler internally creates its own transaction and triggers webhooks via runAsynchronouslyAndWaitUntil after completion. On outer transaction retry, these webhooks fire again, and the permission grants in the outer transaction don't coordinate with the membership created in the nested one.

Use the lower-level addUserToTeam function directly (as done in integrations/custom/projects/transfer/confirm/verification-code-handler.tsx) to keep all operations atomic within a single transaction:

await retryTransaction(prisma, async (tx) => {
  await addUserToTeam(tx, {
    tenancy,
    teamId: data.team_id,
    userId: user.id,
    type: 'member',
  });

  if (data.permission_ids && data.permission_ids.length > 0) {
    const uniquePermissionIds = [...new Set(data.permission_ids)];
    for (const permissionId of uniquePermissionIds) {
      await grantTeamPermission(tx, {
        tenancy,
        teamId: data.team_id,
        userId: user.id,
        permissionId,
      });
    }
  }
});

// Send webhooks after transaction succeeds
await runAsynchronouslyAndWaitUntil(/* webhook calls */);
🤖 Prompt for AI Agents
In
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
around lines 111 to 131, replace the call to
teamMembershipsCrudHandlers.adminCreate inside the retryTransaction with the
lower-level addUserToTeam(tx, { tenancy, teamId: data.team_id, userId: user.id,
type: 'member' }) so all DB changes (membership + permission grants) occur
within the same transaction and avoid nested transactions/webhook duplication;
keep the permission deduplication and grantTeamPermission calls using the
transaction object, and move any webhook or async post-commit work
(runAsynchronouslyAndWaitUntil) to after the retryTransaction completes
successfully.

Comment on lines +5 to +46
export const GET = createSmartRouteHandler({
metadata: {
summary: "Get role-based permissions for team invitations",
description: "Fetch available role-based permissions that can be assigned to team members during invitations. Only returns role-based permissions, not system permissions.",
tags: ["Teams"],
},
request: yupObject({
auth: yupObject({
type: clientOrHigherAuthTypeSchema,
tenancy: adaptSchema.defined(),
user: adaptSchema.optional(),
}).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
items: yupArray(yupObject({
id: yupString().defined(),
description: yupString().optional(),
contained_permission_ids: yupArray(yupString().defined()).defined(),
}).defined()).defined(),
is_paginated: yupBoolean().oneOf([false]).defined(),
}).defined(),
}),
async handler({ auth }) {
const allPermissions = await listPermissionDefinitions({
scope: "team",
tenancy: auth.tenancy,
});

// Return all permissions including system permissions (starting with $)
return {
statusCode: 200,
bodyType: "json",
body: {
items: allPermissions,
is_paginated: false,
},
};
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify whether system permissions should be included.

There's an inconsistency between the metadata description and the implementation:

  • Line 8 states: "Only returns role-based permissions, not system permissions"
  • Line 36 comment states: "Return all permissions including system permissions"
  • The code returns allPermissions without filtering

Please clarify the intended behavior and update either:

  1. The description (line 8) if system permissions should be included, or
  2. The implementation (lines 36-41) to filter out system permissions (those starting with $) if they should be excluded
🤖 Prompt for AI Agents
In apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
around lines 5 to 46: the metadata says this endpoint should return only
role-based permissions (no system permissions) but the handler returns
allPermissions and the inline comment says it returns system permissions—either
update the metadata to state system permissions are included, or filter out
permissions whose ids start with "$" before returning; to fix, decide desired
behavior and then either change the metadata/description/tags to reflect
inclusion, or modify the handler to filter allPermissions (remove items with id
starting with "$") and ensure the response body and response schema still match.

- Only users with `$invite_members` permission can send team invitations
- All permissions (both role-based and system) are validated against your project configuration
- Invalid or non-existent permission IDs are rejected
- System permissions can now be directly assigned through invitations
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resolve contradictory statements about system permissions.

The documentation contains conflicting statements:

  • Line 214 states: "System permissions can now be directly assigned through invitations"
  • Line 275 states: "System permissions cannot be directly assigned through invitations"

Please clarify which statement is correct and update the documentation accordingly to avoid user confusion.

Also applies to: 275-275

🤖 Prompt for AI Agents
In docs/concepts/role-based-team-invitations.mdx around lines 214 and 275, there
are contradictory statements about whether system permissions can be assigned
via invitations; pick the correct behavior (confirm with product/engineering if
unsure) and make both lines consistent: if system permissions are allowed,
change line 275 to match line 214 and add a short clarifying sentence about
which permissions are assignable and any required roles/flags; if they are not
allowed, change line 214 to match line 275 and add a short note explaining how
system permissions must be granted instead (e.g., via admin console or API).
Ensure both instances use identical phrasing and update any nearby examples or
links to reflect the chosen behavior.

@BilalG1 BilalG1 requested a review from N2D4 November 19, 2025 22:10
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)

34-35: permission_ids acceptance and granting logic is sound

The verification-code handler correctly treats permission_ids as optional, deduplicates them via new Set, and applies each via grantTeamPermission, which centralizes existence/scope validation. Note that permissions are only granted on first-time membership creation (!oldMembership), so re-accepting an invite for an already-member won’t adjust permissions—this matches the “bootstrap-on-join” semantics implied by the tests.

If you ever need invitations to adjust permissions for existing members, this if (!oldMembership) guard will need to be revisited or extended.

Also applies to: 118-129

apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)

318-380: New permission_ids-focused tests provide targeted coverage for profile endpoints

The tests includes permission_ids in team member profiles response and returns correct permission_ids for team members with granted permissions effectively verify that both /team-member-profiles/:team_id/me and /team-member-profiles/:team_id/:user_id surface permission_ids and reflect granted permissions like team_admin. This nicely bridges the invite/permission flows with the profile APIs.

If you start returning more metadata on these endpoints, consider factoring out small helpers to reduce duplication across these tests when asserting profile shapes.

apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (2)

6-6: Double-check the auth type used in “requires $invite_members permission to send invitation”

This test now calls /team-invitations/send-code with accessType: "server", but the route’s explicit $invite_members check currently only runs when auth.type === "client", and a later test (allows server access invitation with permission_ids bypassing user permissions) deliberately relies on server access bypassing user-level permission checks. It’s worth confirming whether this first test is still exercising the intended code path or should be using client access (or be renamed to reflect a different guarantee).

Also applies to: 32-32


583-668: permission_ids send/accept, validation, escalation, and compatibility tests comprehensively cover the new behavior

The added tests around sending invitations with permission_ids, applying them on acceptance, handling invalid/malformed/mixed IDs (400 vs 404), blocking client-side elevation attempts with 403, and maintaining backwards compatibility for empty/omitted permission_ids collectively mirror the logic in the send-code and verification handlers. They should catch regressions both in validation and in who is allowed to attach permissions to invites.

If these flows grow further, consider extracting small helper utilities (e.g., “sendInviteWithPermissions”, “expectInvalidPermissionIds404”) to reduce duplication and keep individual tests focused on behavior rather than setup.

Also applies to: 671-760, 762-841, 844-920, 923-955, 999-1027

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38dfd88 and 38664c9.

📒 Files selected for processing (4)
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (5 hunks)
  • apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (4 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (7 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (4)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (729-738)
packages/stack-shared/src/utils/errors.tsx (1)
  • StatusError (152-261)
apps/backend/src/lib/permissions.tsx (1)
  • listPermissionDefinitionsFromConfig (161-181)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)
  • teamInvitationCodeHandler (13-155)
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (4)
apps/e2e/tests/helpers.ts (1)
  • it (12-12)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (2)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (729-738)
apps/backend/src/lib/permissions.tsx (1)
  • grantTeamPermission (96-138)
🪛 Biome (2.1.2)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx

[error] 72-72: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)


[error] 138-138: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

⏰ 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). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: setup-tests
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
🔇 Additional comments (5)
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (2)

5-6: permission_ids schema, validation, and forwarding look consistent with the new invite flow

The request schema, config-based validation, and forwarding into teamInvitationCodeHandler.sendCode are wired coherently: permission_ids is optional, validated against the team-scoped definitions (raising KnownErrors.PermissionNotFound for unknown IDs), and always passed as an array (defaulting to []), which matches the acceptance handler’s expectations and the new e2e tests (404 for invalid IDs, 200 for valid server-side IDs, and 200 when omitted).

Also applies to: 26-27, 57-69, 75-75


40-44: Client-side restriction on permission_ids matches the updated security model

The explicit 403 guard when auth.type === "client" and body.permission_ids is defined aligns with the new tests that assert client requests with permission_ids are blocked, while client requests without permission_ids continue to work and server access can set them. No change requested; just confirming this is now the intended contract.

If the long-term goal is to eventually allow some client flows to set permission_ids, we should capture that in documentation or a follow-up issue since the current behavior is now enforced and covered by tests.

apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)

151-175: Expanded team-member profile snapshots and permission_ids assertions look consistent

The updated snapshots and assertions for permission_ids (both in list responses and single-profile endpoints) align with the new backend behavior: default members show [], creator/admin users show elevated permissions, and the values remain stable across profile updates. The two additional tests that check for the presence and correctness of permission_ids add good coverage without over-constraining the response shape.

Also applies to: 193-223, 233-245, 260-272, 303-315, 165-165, 200-201, 207-207, 214-214, 238-238, 265-265, 308-308

apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (2)

170-171: Invitation list snapshots with permission_ids align with the new API contract

The updated list-invitations snapshots now include permission_ids: [], which matches the server behavior of always returning a permission_ids field (empty array when none are set) and keeps the response shape stable for consumers and follow-up tests.

Also applies to: 241-242


561-581: Role-permissions endpoint tests are well scoped

The tests around /team-invitations/role-permissions validate both structure (items, contained_permission_ids, is_paginated) and behavior for client vs server vs unauthenticated access, and assert that client/server responses are identical. This gives good confidence that the UI can rely on this endpoint for permission selection.

Also applies to: 958-997

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (2)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (2)

763-801: Client-side elevation attempt correctly expects 403

This test now uses accessType: "client" with permission_ids: ["team_admin"] and asserts a 403 plus a clear error string that permission_ids must be server-side. That matches the intended hardening where any client attempt to set permission_ids should be rejected up front.


803-841: Empty client-side permission_ids still rejected with 403

Using permission_ids: [] from a client request and expecting 403 ensures the security check also covers “default/empty” arrays, closing a subtle bypass vector. The string-based assertion on the error message is consistent with the previous test.

🧹 Nitpick comments (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1)

970-997: Client/server parity test for role-permissions is strong but strict

Verifying that client and server responses are both 200 and structurally correct, and then asserting clientResponse.body === serverResponse.body, gives strong guarantees about parity. It is slightly brittle if extra metadata fields diverge in the future; if that happens, you may want to relax this to a subset comparison (e.g., items only), but for now it’s fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38664c9 and 75f1416.

📒 Files selected for processing (1)
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
⏰ 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). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: restart-dev-and-test
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: all-good
  • GitHub Check: docker
  • GitHub Check: build (22.x)
🔇 Additional comments (12)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (12)

161-178: Snapshot updates for permission_ids look consistent

Including "permission_ids": [] in the list-invitations snapshots aligns with the new behavior of always exposing a permission_ids field and keeps older tests compatible with the extended schema. No issues here.

Also applies to: 233-249


561-581: Role-permissions fetch test is clear and sufficiently scoped

This test exercises the /team-invitations/role-permissions endpoint with client access, checks pagination flags, and validates the basic item shape (id, contained_permission_ids as array). Looks correct and focused without over-specifying the schema.


583-620: Server-side invitation with permission_ids is correctly modeled

Using accessType: "server" for sending an invitation with permission_ids: ["team_admin"] matches the security model, and the follow-up list check verifies both presence and exact value of permission_ids. The flow and assertions look sound.


622-668: End-to-end test for applying permission_ids on accept is solid

This test covers the full lifecycle: server-side invitation with permission_ids, accepting via mailbox, then asserting the new member has the expected permission via /team-permissions. The use of some(item.id === "team_admin") is robust enough for unordered results. No issues spotted.


670-700: 404 behavior for non-existent permission_ids is well covered

The test uses server access, sends an obviously invalid permission ID, and verifies a 404 plus details.permission_id. This cleanly documents the error contract for unknown roles and should help prevent regressions.


702-730: Malformed permission_ids validation test is appropriate

Using permission_ids: 123 with server access and asserting a 400 plus an error message mentioning permission_ids and array-ness is a good balance between strictness and flexibility. The regex is broad enough to tolerate minor wording changes while still catching regressions in validation behavior.


732-760: Mixed valid/invalid permission_ids handling looks correct

The test ensures that when permission_ids includes both a valid role and an invalid one, the API responds with 404 and surfaces the offending permission_id. This is a clear, deterministic contract for partial failures and is nicely captured here.


843-881: Back-compat: server-side empty permission_ids is allowed

This test confirms that permission_ids: [] is valid when using accessType: "server", and that invitations get created and listed with permission_ids: []. That gives a clear story: client cannot set permission_ids, but server can explicitly set empty arrays for backward compatibility. Looks good.


883-920: Back-compat: omitted permission_ids from client is preserved

Here, a client request omits permission_ids entirely and is expected to succeed with 200, then list invitations where permission_ids exists but its exact shape is not over-specified. This matches the intended behavior that only explicit client-side permission_ids are blocked, not legacy calls that never send the field.


923-955: Server-access bypass test accurately captures intended power

This test demonstrates that server access can create invitations with permission_ids even if the current user lacks invite permissions, which is consistent with the “server is trusted” model. Listing afterward and asserting permission_ids: ["team_admin"] verifies that the bypass works as designed.


958-968: Unauthenticated access to role-permissions is explicitly specified

By setting backendContext.userAuth to null and still expecting 200 and a valid items array, this test documents that the role-permissions endpoint is public/read-only. That’s useful both for client contracts and for catching accidental auth tightening later.


999-1027: Double-check behavior for permission_ids: null on client requests

This test expects a 400 with a validation-style error for permission_ids: null sent from a client request. Earlier security hardening around permission_ids aimed to reject any client-provided permission_ids up front with a 403 before schema validation. If the route now explicitly allows permission_ids: null to reach validation (to return 400), that’s fine, but it’s worth confirming this matches the latest backend implementation and security intent so tests don’t quietly drift from the guard logic.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on December 10

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

permissionId,
});
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: Permissions not granted for existing team members

When an existing team member accepts an invitation with permission_ids, those permissions are not granted because the permission granting logic is inside the if (!oldMembership) block. This means invitations with specific permissions only work for new members, not for re-inviting existing members with different permissions. If re-inviting existing members is intended to update their permissions, this logic prevents that from working.

Fix in Cursor Fix in Web

@N2D4 N2D4 closed this Nov 20, 2025
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.

3 participants