Skip to content

[codex] Fix org invite visibility and existing-user invite redirects#1877

Open
riderx wants to merge 9 commits intomainfrom
codex/org-invite-status-email
Open

[codex] Fix org invite visibility and existing-user invite redirects#1877
riderx wants to merge 9 commits intomainfrom
codex/org-invite-status-email

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 30, 2026

Summary (AI generated)

  • add explicit pending/active invite status pills in the organization members UI using the same pill pattern as the org dropdown
  • send a dedicated existing-user Bento invite event after successful org invites for users who already have a Capgo account
  • deep-link existing-user invite emails to /dashboard?invite_org=<orgId> and auto-open the existing accept-invite modal from the org switcher
  • preserve the full post-login target in the auth guard so the invite modal still opens after authentication
  • cover the new existing-user invite email endpoint with a focused Vitest integration test

Motivation (AI generated)

Existing org invites already carried enough backend state to distinguish pending invites from accepted memberships, but that state was never surfaced in the members table. Separately, existing-account invites stopped after the RPC created the pending org membership, so no email notification was sent, and even when adding the email path the original redirect would have landed users on the dashboard without opening the invite modal.

Business Impact (AI generated)

This makes org invite state legible for admins, reduces confusion around whether an invite was accepted, and restores email-driven onboarding for invited users who already have an account. The direct modal deep-link should improve acceptance completion and reduce support/debug churn around “I got invited but don’t know what to do next.”

Test Plan (AI generated)

  • bunx eslint src/components/dashboard/DropdownOrganization.vue src/modules/auth.ts src/components/dashboard/InviteTeammateModal.vue src/pages/settings/organization/Members.vue src/utils/invites.ts supabase/functions/_backend/private/send_existing_user_org_invite.ts tests/private-send-existing-user-org-invite.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/private-send-existing-user-org-invite.test.ts
  • bun run typecheck
  • Manually invite an existing user and confirm the email link opens the accept-invite modal for the correct org

Generated with AI

Summary by CodeRabbit

  • New Features

    • Invitations auto-open from invite links; invite acceptance dialog is handled automatically.
    • Existing users now receive a notification when invited to organizations.
  • UI

    • Member list shows inline status pills (pending vs active).
  • Bug Fixes

    • Auth redirect logic refined to preserve invite flows and correct return paths.
    • Organization fetch preserves invite-only orgs for invite deep-links.
  • Localization

    • Added message for failed invite email notifications.
  • Tests

    • Added tests covering the invite notification endpoint and flows.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Auto-opens organization invitation dialogs from an invite_org query, preserves invite-only orgs in the store, adds a new Edge Function + client utility to notify existing users of invites, updates auth redirect logic to use fullPath and structured router objects, adds tests and an i18n message for notification failures.

Changes

Cohort / File(s) Summary
Frontend: invite routing & UI
src/components/dashboard/DropdownOrganization.vue, src/components/dashboard/InviteTeammateModal.vue, src/pages/settings/organization/Members.vue
Detects invite_org query and auto-opens invitation dialog (with handledInviteOrgId dedupe); clears query via router.replace(); calls notifyExistingUserInvite() when invite RPC returns 'OK'; refactors member role/status rendering and adds invite status helpers.
Auth guard
src/modules/auth.ts
Adds invite_org awareness to onboarding redirect logic via shouldRedirectToOrgOnboarding(), uses route navigation objects for redirects, and uses to.fullPath for return URLs.
Organization store
src/stores/organization.ts
Preserves fetched organizations when none are selectable so invite-only orgs remain accessible; only resets current org/role/flags.
Invite utility
src/utils/invites.ts
Adds export async function notifyExistingUserInvite(supabase, email, orgId): Promise<boolean> to call the new Edge Function and surface success/failure.
Edge Function (backend)
supabase/functions/_backend/private/send_existing_user_org_invite.ts, supabase/functions/private/index.ts
New Hono POST handler validating body/permissions, verifying org/user/membership invite state, enforcing resend cooldown with advisory locks and in-memory/cache guards, tracking via Bento, and returning { status: 'ok' } on success; route registered in private index.
Tests
tests/private-send-existing-user-org-invite.test.ts
New Vitest suite for the Edge Function covering validation, auth, not-found, conflict, and success flows; seeds and tears down DB state per test.
i18n
messages/en.json
Added org-invite-email-notification-failed message.
Misc (dev tooling)
src/auto-imports.d.ts
Trailing newline added (no functional change).

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser as Browser/Router
    participant Frontend as Frontend App
    participant EdgeFn as Edge Function
    participant DB as Database
    participant Bento as Bento Tracking

    User->>Browser: Open URL with ?invite_org=<gid>
    Browser->>Frontend: Navigate (route includes invite_org)
    Frontend->>Frontend: fetchOrganizations()
    Frontend->>Frontend: openInvitationFromRouteIfNeeded()
    Frontend->>Browser: router.replace() to remove invite_org

    User->>Frontend: Confirm invite / trigger notify
    Frontend->>EdgeFn: POST /private/send_existing_user_org_invite { email, org_id }
    EdgeFn->>DB: lookup org, inviter, invited user, org_membership
    EdgeFn->>DB: verify membership.user_right startsWith("invite_")
    EdgeFn->>Bento: emit org:invite_existing_capgo_user_to_org
    Bento-->>EdgeFn: ack
    EdgeFn-->>Frontend: { status: "ok" }
    Frontend->>User: show success toast or failure toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I sniff the query, find an invite in sight,
I nudge the router gently and open the light.
A POST to the edge, a Bento gives a cheer,
The invite hops onward, welcome friends are near. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main changes: fixing org invite visibility in the UI and addressing existing-user invite redirects in the auth flow.
Description check ✅ Passed The description provides a summary, test plan, and business impact context. However, it lacks a detailed step-by-step manual testing procedure and screenshots as indicated in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/org-invite-status-email

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Mar 30, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/org-invite-status-email (7d05480) with main (3fd1ec7)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review March 30, 2026 21:28
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7729f8f028

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
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

🧹 Nitpick comments (5)
supabase/functions/_backend/private/send_existing_user_org_invite.ts (2)

20-38: Mixed error handling patterns: throw vs return.

validateRequest throws simpleError for validation failures but returns a Response for authorization failures. This forces the caller to handle both exceptions and Response values. Consider returning consistently (either always throw or always return).

♻️ Consistent return-based approach
 async function validateRequest(c: Context, rawBody: unknown) {
   const validationResult = sendInviteSchema.safeParse(rawBody)
   if (!validationResult.success) {
-    throw simpleError('invalid_request', 'Invalid request', { errors: z.prettifyError(validationResult.error) })
+    return quickError(400, 'invalid_request', 'Invalid request', { errors: z.prettifyError(validationResult.error) })
   }

   const body = validationResult.data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/send_existing_user_org_invite.ts` around
lines 20 - 38, The validateRequest function mixes throwing (simpleError on
validation failure) with returning Response-like values (quickError on auth
failure); make the behavior consistent by returning Response-like errors for
both cases instead of throwing. Update validateRequest to return quickError(400,
'invalid_request', ...) with z.prettifyError(validationResult.error) when
sendInviteSchema.safeParse fails, keeping the existing quickError(403, ...)
path; ensure callers of validateRequest expect and handle the returned { body,
canUpdateUserRoles } or a quickError Response rather than catching exceptions
from simpleError. Reference: validateRequest, sendInviteSchema, simpleError,
quickError, checkPermission.

40-52: Missing requestId usage for structured logging.

Per coding guidelines, backend endpoints should use c.get('requestId') for structured logging with cloudlog(). This endpoint performs multiple database operations and a Bento event but has no logging for debugging or audit purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/send_existing_user_org_invite.ts` around
lines 40 - 52, Retrieve the requestId via c.get('requestId') at the start of the
handler and use cloudlog() for structured logging around key steps: after
successful validation (log requestId, inviterId, canUpdateUserRoles and a
redacted summary of body from validateRequest), before/after each major DB
operation and before emitting the Bento event (include identifiers like
targetUserId/orgId if present), and on error paths (including the quickError 401
branch) so failures include requestId and contextual fields; update
send_existing_user_org_invite.ts to call cloudlog({ requestId, inviterId, ... })
in the parseBody/validateRequest success path, around database calls, and prior
to the Bento event emission.
tests/private-send-existing-user-org-invite.test.ts (2)

59-73: Test covers happy path only.

The test validates the success case but doesn't cover error scenarios such as:

  • Invalid email format
  • Non-existent user
  • Already accepted invitation (409 response)
  • Missing permissions (403 response)

Consider adding edge case tests in a follow-up to improve coverage.

Would you like me to generate additional test cases for these error scenarios?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/private-send-existing-user-org-invite.test.ts` around lines 59 - 73,
Add edge-case tests alongside the existing happy-path test for the POST
/private/send_existing_user_org_invite endpoint: create tests that call the same
endpoint with (1) an invalid email format (use USER_EMAIL_NONMEMBER or a new
invalid string) and assert a 400/validation error, (2) a non-existent user email
and assert a 404, (3) a user who has already accepted the invite and assert a
409, and (4) a caller without sufficient permissions (alter authHeaders or use a
non-admin token) and assert a 403; reuse testOrgId, authHeaders, and the
existing test structure to set up requests and assertions so the new tests
mirror the style of the current "returns ok for an existing user with a pending
org invitation" spec.

60-60: Consider using it.concurrent() for parallel test execution.

Per coding guidelines, tests should use it.concurrent() when possible. This test appears to have isolated test data (unique UUIDs) and should be safe for concurrent execution.

♻️ Use concurrent test
-  it('returns ok for an existing user with a pending org invitation', async () => {
+  it.concurrent('returns ok for an existing user with a pending org invitation', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/private-send-existing-user-org-invite.test.ts` at line 60, The test
"returns ok for an existing user with a pending org invitation" should be
changed to run concurrently: replace the test declaration it('returns ok for an
existing user with a pending org invitation', async () => { ... }) with
it.concurrent('returns ok for an existing user with a pending org invitation',
async () => { ... }); ensure the test body (UUID generation and any shared
fixtures) remains isolated so no shared state prevents concurrent execution and
update only the test declaration in
tests/private-send-existing-user-org-invite.test.ts.
src/components/dashboard/InviteTeammateModal.vue (1)

244-252: Invite notification failure is silently ignored.

The notifyExistingUserInvite call is awaited but its return value (success/failure) is not used. If the email notification fails, the user still sees the success toast. This appears intentional since the invite itself succeeded, but consider logging or showing a warning if the notification fails.

💡 Optional: Add warning on notification failure
     if (data === 'OK') {
-      await notifyExistingUserInvite(supabase, email, orgId)
+      const notified = await notifyExistingUserInvite(supabase, email, orgId)
+      if (!notified) {
+        console.warn('Failed to send invite email notification, but invite was created')
+      }
       toast.success(t('org-invited-user'))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/InviteTeammateModal.vue` around lines 244 - 252, The
invite notification call notifyExistingUserInvite(supabase, email, orgId) is
awaited but any failure is ignored, so update the block that handles the 'OK'
response to catch notification errors: wrap the notifyExistingUserInvite call in
a try/catch (or check its returned success flag) and if it fails log the error
(e.g., console.error or a logger) and surface a non-blocking warning to the user
(e.g., toast.warning('Could not send invite email') or similar) while still
calling completeInviteSuccess({ email, firstName: '', lastName: '' }) and
showing the original success toast; keep toast.success for the invite result but
add the warning/log on notification failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/dashboard/DropdownOrganization.vue`:
- Line 33: The component-level ref handledInviteOrgId is preventing re-opening
the invite modal across the SPA session; change the behavior so invite deduping
is scoped to a single open-attempt rather than the whole component lifetime:
either make handledInviteOrgId a local variable inside
openInvitationFromRouteIfNeeded() (so it’s reset on each invocation) or
explicitly reset handledInviteOrgId when you call clearInviteOrgQuery() or when
the invitation dialog closes (e.g., in the handler that closes the modal).
Update logic in openInvitationFromRouteIfNeeded(), clearInviteOrgQuery(), and
the invite-close handler so the invite_org param can trigger the modal again if
the user re-clicks the same link in the same session. Ensure you keep references
to handledInviteOrgId, openInvitationFromRouteIfNeeded, and clearInviteOrgQuery
when locating the spots to change.

In `@src/modules/auth.ts`:
- Line 151: The two email verification redirects to '/resend_email' are
inconsistent: one sets return_to: to.fullPath while the other uses to.path
(losing query params like ?invite_org=). Update the second redirect (the branch
handling the already-authenticated but unverified email — the block checking the
'email_not_verified' condition that issues a redirect to '/resend_email') to set
return_to: to.fullPath instead of to.path so both redirects behave identically
and preserve query parameters; search for the redirect to '/resend_email' and
the return_to key in auth.ts to locate and fix the code.

In `@supabase/functions/_backend/private/send_existing_user_org_invite.ts`:
- Around line 11-14: The sendInviteSchema uses z.string().check(z.minLength(1))
for org_id which is invalid in Zod v4; update the schema so org_id is defined as
z.string().min(1) (i.e., change the org_id line inside sendInviteSchema to use
z.string().min(1)) and keep the email as z.email(); this will ensure proper
minimum-length validation with Zod 4.
- Around line 16-18: Replace the Hono instance construction so the endpoint uses
the centralized middleware: import createHono from ../utils/hono.ts, replace new
Hono<MiddlewareKeyVariables>() with createHono('', version), and keep existing
middleware (e.g., app.use('/', useCors)) so app is created via createHono to
include logger, requestId, and X-Worker-Source headers; ensure the createHono
import and the version variable are present in the file.

---

Nitpick comments:
In `@src/components/dashboard/InviteTeammateModal.vue`:
- Around line 244-252: The invite notification call
notifyExistingUserInvite(supabase, email, orgId) is awaited but any failure is
ignored, so update the block that handles the 'OK' response to catch
notification errors: wrap the notifyExistingUserInvite call in a try/catch (or
check its returned success flag) and if it fails log the error (e.g.,
console.error or a logger) and surface a non-blocking warning to the user (e.g.,
toast.warning('Could not send invite email') or similar) while still calling
completeInviteSuccess({ email, firstName: '', lastName: '' }) and showing the
original success toast; keep toast.success for the invite result but add the
warning/log on notification failure.

In `@supabase/functions/_backend/private/send_existing_user_org_invite.ts`:
- Around line 20-38: The validateRequest function mixes throwing (simpleError on
validation failure) with returning Response-like values (quickError on auth
failure); make the behavior consistent by returning Response-like errors for
both cases instead of throwing. Update validateRequest to return quickError(400,
'invalid_request', ...) with z.prettifyError(validationResult.error) when
sendInviteSchema.safeParse fails, keeping the existing quickError(403, ...)
path; ensure callers of validateRequest expect and handle the returned { body,
canUpdateUserRoles } or a quickError Response rather than catching exceptions
from simpleError. Reference: validateRequest, sendInviteSchema, simpleError,
quickError, checkPermission.
- Around line 40-52: Retrieve the requestId via c.get('requestId') at the start
of the handler and use cloudlog() for structured logging around key steps: after
successful validation (log requestId, inviterId, canUpdateUserRoles and a
redacted summary of body from validateRequest), before/after each major DB
operation and before emitting the Bento event (include identifiers like
targetUserId/orgId if present), and on error paths (including the quickError 401
branch) so failures include requestId and contextual fields; update
send_existing_user_org_invite.ts to call cloudlog({ requestId, inviterId, ... })
in the parseBody/validateRequest success path, around database calls, and prior
to the Bento event emission.

In `@tests/private-send-existing-user-org-invite.test.ts`:
- Around line 59-73: Add edge-case tests alongside the existing happy-path test
for the POST /private/send_existing_user_org_invite endpoint: create tests that
call the same endpoint with (1) an invalid email format (use
USER_EMAIL_NONMEMBER or a new invalid string) and assert a 400/validation error,
(2) a non-existent user email and assert a 404, (3) a user who has already
accepted the invite and assert a 409, and (4) a caller without sufficient
permissions (alter authHeaders or use a non-admin token) and assert a 403; reuse
testOrgId, authHeaders, and the existing test structure to set up requests and
assertions so the new tests mirror the style of the current "returns ok for an
existing user with a pending org invitation" spec.
- Line 60: The test "returns ok for an existing user with a pending org
invitation" should be changed to run concurrently: replace the test declaration
it('returns ok for an existing user with a pending org invitation', async () =>
{ ... }) with it.concurrent('returns ok for an existing user with a pending org
invitation', async () => { ... }); ensure the test body (UUID generation and any
shared fixtures) remains isolated so no shared state prevents concurrent
execution and update only the test declaration in
tests/private-send-existing-user-org-invite.test.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b25cafc-d63b-410d-9afc-85e44b3bd94c

📥 Commits

Reviewing files that changed from the base of the PR and between f53ec7c and 7729f8f.

📒 Files selected for processing (8)
  • src/components/dashboard/DropdownOrganization.vue
  • src/components/dashboard/InviteTeammateModal.vue
  • src/modules/auth.ts
  • src/pages/settings/organization/Members.vue
  • src/utils/invites.ts
  • supabase/functions/_backend/private/send_existing_user_org_invite.ts
  • supabase/functions/private/index.ts
  • tests/private-send-existing-user-org-invite.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af81685e66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
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: 2

Caution

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

⚠️ Outside diff range comments (2)
src/stores/organization.ts (1)

397-411: ⚠️ Potential issue | 🟠 Major

Filter invite-only orgs out of the app-id index.

Keeping _organizations populated here is fine for the switcher, but the watcher at Lines 247-287 now also indexes apps for pending invites. If an app id gets mapped, hasPermissionsInRole() will treat invite_* as an active role because normalizeLegacyRole() strips the prefix. Preserve invite-only orgs for the dropdown, but build _organizationsByAppId only from selectable orgs.

💡 Suggested fix (outside this hunk)
-    const organizations = Array.from(organizationsMap.values())
+    const organizations = Array.from(organizationsMap.values())
+      .filter(org => isSelectableOrganization(org.role))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/organization.ts` around lines 397 - 411, The _organizations map
correctly keeps invite-only orgs for the switcher, but the watcher is indexing
apps from all orgs which causes invite_* roles to be treated as active; change
the code that builds _organizationsByAppId (the app-id index) to iterate only
over selectableOrganizations (the result of
mappedData.filter(isSelectableOrganization)) instead of mappedData; ensure the
logic that sets _organizationsByAppId.value (and any helper that populates it)
uses selectableOrganizations so invite-only orgs remain in _organizations but
are excluded from the app-id index used by
hasPermissionsInRole/normalizeLegacyRole.
src/components/dashboard/DropdownOrganization.vue (1)

113-121: ⚠️ Potential issue | 🟠 Major

Scope invite rejection to the current org.

The deny path only filters org_users by user_id. Declining one invite will delete every row this user is allowed to delete, not just the invitation for org.gid. Add the org predicate too (org_id if that's the FK column here).

💡 Suggested fix
           const { error } = await supabase
             .from('org_users')
             .delete()
+            .eq('org_id', org.gid)
             .eq('user_id', userId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/DropdownOrganization.vue` around lines 113 - 121,
The delete handler currently removes org_users rows by only filtering on user_id
(see the handler async function and the
supabase.from('org_users').delete().eq('user_id', userId) call), which can
delete invites across all orgs; update that query to also filter by the current
organization (add an .eq('org_id', org.gid) or the correct FK column name) so
the delete targets only the invite for the current org; ensure you read the org
identifier from the component state (org.gid) and include it in the supabase
query before executing the delete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/auth.ts`:
- Around line 99-100: The current logic sets hasInviteOrgQuery true for any
non-empty invite_org query and lets shouldRedirectToOrgOnboarding skip
onboarding even if the invite is stale; change this to defer the invite check
until after fetchOrganizations() completes: read invite_gid from
to.query.invite_org, call fetchOrganizations(), then check the loaded
organizations list for an entry with gid === invite_gid and a role that
startsWith('invite') (or matches invite*), and only then set
shouldRedirectToOrgOnboarding to false; update the same pattern used around
hasInviteOrgQuery/shouldRedirectToOrgOnboarding at the other occurrences (the
blocks around the noted alternate locations) so stale/revoked invites no longer
suppress onboarding and DropdownOrganization.vue receives a valid org context.

In `@tests/private-send-existing-user-org-invite.test.ts`:
- Around line 21-61: The shared fixture created in beforeAll (the
org/stripe/org_users seeded via getSupabaseClient() using
testOrgId/testCustomerId and users USER_ID/USER_ID_NONMEMBER) is mutated by one
test, preventing safe parallel runs; change to per-test seeding: move the
org/stripe/org_users inserts from beforeAll into beforeEach (or have the
mutating test create its own dedicated org and org_users) so each test gets an
isolated org id and customer id (generate a unique testOrgId/testCustomerId per
test) and avoid reusing the shared org_users entry that includes
USER_ID_NONMEMBER; ensure any test that modifies membership seeds its own data
rather than relying on the global beforeAll fixture.

---

Outside diff comments:
In `@src/components/dashboard/DropdownOrganization.vue`:
- Around line 113-121: The delete handler currently removes org_users rows by
only filtering on user_id (see the handler async function and the
supabase.from('org_users').delete().eq('user_id', userId) call), which can
delete invites across all orgs; update that query to also filter by the current
organization (add an .eq('org_id', org.gid) or the correct FK column name) so
the delete targets only the invite for the current org; ensure you read the org
identifier from the component state (org.gid) and include it in the supabase
query before executing the delete.

In `@src/stores/organization.ts`:
- Around line 397-411: The _organizations map correctly keeps invite-only orgs
for the switcher, but the watcher is indexing apps from all orgs which causes
invite_* roles to be treated as active; change the code that builds
_organizationsByAppId (the app-id index) to iterate only over
selectableOrganizations (the result of
mappedData.filter(isSelectableOrganization)) instead of mappedData; ensure the
logic that sets _organizationsByAppId.value (and any helper that populates it)
uses selectableOrganizations so invite-only orgs remain in _organizations but
are excluded from the app-id index used by
hasPermissionsInRole/normalizeLegacyRole.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bc045f8-7780-4aa2-aac3-d95cdec1c6fa

📥 Commits

Reviewing files that changed from the base of the PR and between 7729f8f and af81685.

📒 Files selected for processing (7)
  • messages/en.json
  • src/components/dashboard/DropdownOrganization.vue
  • src/components/dashboard/InviteTeammateModal.vue
  • src/modules/auth.ts
  • src/stores/organization.ts
  • supabase/functions/_backend/private/send_existing_user_org_invite.ts
  • tests/private-send-existing-user-org-invite.test.ts
✅ Files skipped from review due to trivial changes (2)
  • messages/en.json
  • supabase/functions/_backend/private/send_existing_user_org_invite.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/dashboard/InviteTeammateModal.vue

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: daa9a1aa1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
supabase/functions/_backend/private/send_existing_user_org_invite.ts (1)

17-18: In-memory cooldown map is not shared across Edge Function instances.

The inviteNotificationCooldowns Map only provides per-instance rate limiting. In a distributed environment (Supabase Edge Functions or Cloudflare Workers), different requests may hit different instances, bypassing the in-memory check. The CacheHelper provides the durable layer, but the in-memory map offers minimal value and may give a false sense of protection.

Consider removing the in-memory map and relying solely on CacheHelper, or document that it's purely an optimization for rapid successive requests within the same instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/send_existing_user_org_invite.ts` around
lines 17 - 18, The in-memory Map inviteNotificationCooldowns provides only
per-instance cooldowns and should be removed; delete the
inviteNotificationCooldowns declaration and any checks/updates that reference
it, and instead rely solely on CacheHelper for durable cooldown checks and
writes using the existing INVITE_RESEND_COOLDOWN_MINUTES as the TTL; ensure all
cooldown logic in send_existing_user_org_invite (or any helper called by it)
uses CacheHelper.get/check and CacheHelper.set with a deterministic key (e.g.,
user/org invite key) and TTL derived from INVITE_RESEND_COOLDOWN_MINUTES so
distributed instances respect the same cooldown.
tests/private-send-existing-user-org-invite.test.ts (1)

77-169: Good test isolation with per-test fixtures; consider adding coverage for edge cases.

The test suite correctly uses it.concurrent() with isolated fixtures per test, addressing the previous review feedback. The cleanup in finally blocks ensures proper teardown.

However, a few endpoint behaviors are not covered:

  • Organization not found (404 organization_not_found)
  • Cooldown/rate limiting (409 user_already_invited)
  • invite_super_admin permission escalation check

Would you like me to generate additional test cases for these edge cases?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/private-send-existing-user-org-invite.test.ts` around lines 77 - 169,
Add three new concurrent tests to cover missing edge cases: (1) "organization
not found" — call postSendExistingUserOrgInvite with a non-existent org_id
(e.g., fixture.orgId + '-missing') and assert response.status === 404 and
returned error === 'organization_not_found'; (2) "cooldown / already invited" —
simulate a prior invite by inserting an org_users row or calling
postSendExistingUserOrgInvite twice for the same email/org and assert
response.status === 409 and error === 'user_already_invited'; and (3)
"invite_super_admin permission escalation" — attempt to invite with credentials
lacking super-admin rights (use nonMemberAuthHeaders or create a low-priv
fixture) targeting a payload that would grant elevated rights and assert
response.status === 403 and error === 'not_authorized'; reuse
createInviteTestFixture, authHeaders, nonMemberAuthHeaders,
USER_EMAIL_NONMEMBER, USER_ID_NONMEMBER and ensure each test uses its own
fixture and cleanup in finally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@supabase/functions/_backend/private/send_existing_user_org_invite.ts`:
- Around line 17-18: The in-memory Map inviteNotificationCooldowns provides only
per-instance cooldowns and should be removed; delete the
inviteNotificationCooldowns declaration and any checks/updates that reference
it, and instead rely solely on CacheHelper for durable cooldown checks and
writes using the existing INVITE_RESEND_COOLDOWN_MINUTES as the TTL; ensure all
cooldown logic in send_existing_user_org_invite (or any helper called by it)
uses CacheHelper.get/check and CacheHelper.set with a deterministic key (e.g.,
user/org invite key) and TTL derived from INVITE_RESEND_COOLDOWN_MINUTES so
distributed instances respect the same cooldown.

In `@tests/private-send-existing-user-org-invite.test.ts`:
- Around line 77-169: Add three new concurrent tests to cover missing edge
cases: (1) "organization not found" — call postSendExistingUserOrgInvite with a
non-existent org_id (e.g., fixture.orgId + '-missing') and assert
response.status === 404 and returned error === 'organization_not_found'; (2)
"cooldown / already invited" — simulate a prior invite by inserting an org_users
row or calling postSendExistingUserOrgInvite twice for the same email/org and
assert response.status === 409 and error === 'user_already_invited'; and (3)
"invite_super_admin permission escalation" — attempt to invite with credentials
lacking super-admin rights (use nonMemberAuthHeaders or create a low-priv
fixture) targeting a payload that would grant elevated rights and assert
response.status === 403 and error === 'not_authorized'; reuse
createInviteTestFixture, authHeaders, nonMemberAuthHeaders,
USER_EMAIL_NONMEMBER, USER_ID_NONMEMBER and ensure each test uses its own
fixture and cleanup in finally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f401019a-2314-434d-bb53-3dd630d7e4aa

📥 Commits

Reviewing files that changed from the base of the PR and between daa9a1a and e5af050.

📒 Files selected for processing (5)
  • src/auto-imports.d.ts
  • src/modules/auth.ts
  • src/pages/settings/organization/Members.vue
  • supabase/functions/_backend/private/send_existing_user_org_invite.ts
  • tests/private-send-existing-user-org-invite.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/auto-imports.d.ts
  • src/pages/settings/organization/Members.vue

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5af050c85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 322f95ee42

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sonarqubecloud
Copy link
Copy Markdown

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