Skip to content

Commit 232d735

Browse files
authored
fix(payments): add org membership check to top-up checkout (#1414)
## Summary - Adds an organization authorization check to the top-up checkout route (`/payments/topup`) - Previously, any authenticated user could initiate a checkout for any organization by passing an arbitrary `organization-id` query param - Now uses `getAuthorizedOrgContext` to verify the user is a member of the org (or an admin) before creating/reusing the org's Stripe customer ## Test plan - [ ] Verify top-up works normally for a user's own account (no `organization-id`) - [ ] Verify top-up works for an org the user belongs to - [ ] Verify top-up returns 404 when `organization-id` belongs to an org the user is not a member of - [ ] Verify admin users can still top up any org
2 parents ff0ba33 + 07d5fe8 commit 232d735

1 file changed

Lines changed: 11 additions & 4 deletions

File tree

src/app/payments/topup/route.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MAXIMUM_TOP_UP_AMOUNT, MINIMUM_TOP_UP_AMOUNT } from '@/lib/constants';
66
import { isValidReturnUrl } from '@/lib/payment-return-url';
77
import { captureException } from '@sentry/nextjs';
88
import { getOrCreateStripeCustomerIdForOrganization } from '@/lib/organizations/organization-billing';
9+
import { getAuthorizedOrgContext } from '@/lib/organizations/organization-auth';
910

1011
/**
1112
* NOTE: Crypto payment support (Coinbase Commerce) was removed in January 2026.
@@ -63,10 +64,16 @@ export async function POST(request: NextRequest): Promise<NextResponse<unknown>>
6364
return NextResponse.json({ error: 'Invalid org id' }, { status: 400 });
6465
}
6566

66-
const stripeCustomerId = organizationId
67-
? // TODO(bmc): should we check user permission to organization here?
68-
await getOrCreateStripeCustomerIdForOrganization(organizationId)
69-
: currentUser.stripe_customer_id;
67+
let stripeCustomerId: string | null | undefined;
68+
if (organizationId) {
69+
const orgContext = await getAuthorizedOrgContext(organizationId, ['owner', 'billing_manager']);
70+
if (!orgContext.success) {
71+
return orgContext.nextResponse;
72+
}
73+
stripeCustomerId = await getOrCreateStripeCustomerIdForOrganization(organizationId);
74+
} else {
75+
stripeCustomerId = currentUser.stripe_customer_id;
76+
}
7077

7178
const cancelPathRaw = searchParams.get('cancel-path');
7279
const cancelPath = cancelPathRaw && isValidReturnUrl(cancelPathRaw) ? cancelPathRaw : null;

0 commit comments

Comments
 (0)