fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: enforce team scoping for campaign, contacts, and invites - #356

Merged
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits into
mainfrom
fix/team-scope-authorization

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Feb 22, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel

vercelBot commented Feb 22, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentFeb 23, 2026 0:28am

@coderabbitai

coderabbitaiBot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

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


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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Feb 22, 2026

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:ce4eec8
Status: ✅ Deploy successful!
Preview URL:https://26462adc.usesend.pages.dev
Branch Preview URL:https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
 const invite = await db.teamInvite.findFirst({
where: {
teamId,
- id: {- equals: inviteId,- },+ id: inviteId,
},
});

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik
KMKoushik merged commit 61dfcee into mainFeb 23, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites
* fix(test): mock domain service in campaign security test
Sign up for freeto 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

@KMKoushik