Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from backend.util.exceptions import NotFoundError

from . import db as org_db
from .model import CreateInvitationRequest, InvitationCreateResponse, InvitationResponse
from .model import (
CreateInvitationRequest,
InvitationCreateResponse,
InvitationResponse,
UserInvitationResponse,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -378,7 +383,7 @@ async def decline_invitation(
)
async def list_pending_for_user(
user_id: Annotated[str, Security(get_user_id)],
) -> list[InvitationResponse]:
) -> list[UserInvitationResponse]:
# Get user's email
user = await prisma.user.find_unique(where={"id": user_id})
if user is None:
Expand All @@ -391,6 +396,7 @@ async def list_pending_for_user(
"revokedAt": None,
"expiresAt": {"gt": datetime.now(timezone.utc)},
},
include={"Org": True},
order={"createdAt": "desc"},
)
return [InvitationResponse.from_db(inv) for inv in invitations]
return [UserInvitationResponse.from_db(inv) for inv in invitations]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pending invites use exact email

Medium Severity

GET /invitations/pending looks up invitations with an exact email match, while accept and decline already compare emails case-insensitively. The new invite form also sends the typed address unchanged, and invite email delivery is still a TODO, so a mixed-case invite never appears in You've been invited and cannot be accepted from the UI.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit af9942f. Configure here.

33 changes: 33 additions & 0 deletions autogpt_platform/backend/backend/api/features/orgs/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,36 @@ def from_db(inv) -> "InvitationCreateResponse":
created_at=inv.createdAt,
team_ids=inv.teamIds,
)


class UserInvitationResponse(BaseModel):
"""Invitation as seen by the INVITEE (GET /invitations/pending).

Includes the accept/decline token — safe here because the list is
filtered to the caller's own email — and the inviting org's display
info, without which the invitee can't tell who is inviting them.
"""

id: str
token: str
org_id: str
org_name: str
org_slug: str
is_admin: bool
is_billing_manager: bool
expires_at: datetime
created_at: datetime

@staticmethod
def from_db(inv) -> "UserInvitationResponse":
return UserInvitationResponse(
id=inv.id,
token=inv.token,
org_id=inv.orgId,
org_name=inv.Org.name if inv.Org else "",
org_slug=inv.Org.slug if inv.Org else "",
is_admin=inv.isAdmin,
is_billing_manager=inv.isBillingManager,
expires_at=inv.expiresAt,
created_at=inv.createdAt,
)
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,10 @@ def test_list_pending_for_user(self, _app_and_client):
inv.isAdmin = False
inv.isBillingManager = False
inv.token = "tok-1"
inv.orgId = "org-1"
inv.Org = MagicMock()
inv.Org.name = "Acme Org"
inv.Org.slug = "acme-org"
inv.expiresAt = datetime.now(timezone.utc) + timedelta(days=5)
inv.createdAt = FIXED_NOW
inv.teamIds = []
Expand All @@ -1631,7 +1635,10 @@ def test_list_pending_for_user(self, _app_and_client):
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["email"] == "test@example.com"
assert data[0]["token"] == "tok-1"
assert data[0]["org_id"] == "org-1"
assert data[0]["org_name"] == "Acme Org"
assert data[0]["org_slug"] == "acme-org"

def test_list_pending_no_user_returns_empty(self, _app_and_client):
_, client = _app_and_client
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BrainIcon,
BuildingIcon,
ChartIncreaseIcon,
CreditCardIcon,
Key01Icon,
Expand Down Expand Up @@ -32,6 +33,11 @@ export const settingsNavItems: SettingsNavItem[] = [
flag: Flag.GRAPHITI_MEMORY,
},
{ label: "Billing", href: "/settings/billing", Icon: CreditCardIcon },
{
label: "Organization",
href: "/settings/organization",
Icon: BuildingIcon,
},
Comment thread
cursor[bot] marked this conversation as resolved.
Comment on lines +36 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The "Organization" settings nav item and page are unconditionally visible, bypassing the SHOW_ORG_SETTINGS feature flag because the flag is not checked in the sidebar or on the page itself.
Severity: MEDIUM

Suggested Fix

Add flag: Flag.SHOW_ORG_SETTINGS to the "Organization" item in settingsNavItems in helpers.ts. Then, update the filter logic in useSettingsSidebar.ts to correctly check for all feature flags, not just GRAPHITI_MEMORY. Finally, add a feature flag guard to the organization settings page (page.tsx) to prevent direct access.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
autogpt_platform/frontend/src/app/(platform)/settings/components/SettingsSidebar/helpers.ts#L36-L40

Potential issue: The "Organization" navigation item in `settingsNavItems` is defined
without the `flag: Flag.SHOW_ORG_SETTINGS` property. Furthermore, the filter in
`useSettingsSidebar` only gates items based on `Flag.GRAPHITI_MEMORY`, not other flags.
Consequently, the "Organization" item always appears in the settings sidebar. The
corresponding page at `/settings/organization` also lacks a feature flag guard, making
it unconditionally accessible. This exposes an incomplete UI to all users, contrary to
the stated goal of keeping it behind the `SHOW_ORG_SETTINGS` flag.

Also affects:

  • autogpt_platform/frontend/src/app/(platform)/settings/organization/page.tsx:1

{
label: "Integrations",
href: "/settings/integrations",
Expand Down
Loading
Loading