Skip to content

Commit 9bba7b7

Browse files
committed
fix(billing): require org owner role for Stripe checkout URL creation
Previously getSubscriptionStripeUrl used baseProcedure and only enforced ownership when existing subscriptions were found, allowing unauthenticated Stripe customer creation for new orgs. Switch to organizationOwnerProcedure to enforce ownership unconditionally.
1 parent dc756e5 commit 9bba7b7

2 files changed

Lines changed: 27 additions & 9 deletions

File tree

src/routers/organizations/organization-subscription-router.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,32 @@ describe('organizations subscription trpc router', () => {
5757
});
5858
});
5959

60+
describe('getSubscriptionStripeUrl procedure', () => {
61+
it('should throw UNAUTHORIZED error for non-owner members', async () => {
62+
const caller = await createCallerForUser(memberUser.id);
63+
64+
await expect(
65+
caller.organizations.subscription.getSubscriptionStripeUrl({
66+
organizationId: testOrganization.id,
67+
seats: 1,
68+
cancelUrl: 'https://example.com',
69+
})
70+
).rejects.toThrow('You do not have the required organizational role to access this feature');
71+
});
72+
73+
it('should throw UNAUTHORIZED error for non-member users', async () => {
74+
const caller = await createCallerForUser(_nonMemberUser.id);
75+
76+
await expect(
77+
caller.organizations.subscription.getSubscriptionStripeUrl({
78+
organizationId: testOrganization.id,
79+
seats: 1,
80+
cancelUrl: 'https://example.com',
81+
})
82+
).rejects.toThrow('You do not have access to this organization');
83+
});
84+
});
85+
6086
describe('cancel procedure', () => {
6187
it('should throw UNAUTHORIZED error for non-owner users', async () => {
6288
const caller = await createCallerForUser(memberUser.id);

src/routers/organizations/organization-subscription-router.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { baseProcedure, createTRPCRouter } from '@/lib/trpc/init';
1515
import {
1616
OrganizationIdInputSchema,
1717
organizationOwnerProcedure,
18-
ensureOrganizationAccess,
1918
organizationMemberProcedure,
2019
} from '@/routers/organizations/utils';
2120
import { TRPCError } from '@trpc/server';
@@ -117,7 +116,7 @@ export const organizationsSubscriptionRouter = createTRPCRouter({
117116
return { status: paymentStatus };
118117
}),
119118

120-
getSubscriptionStripeUrl: baseProcedure
119+
getSubscriptionStripeUrl: organizationOwnerProcedure
121120
.input(SubscriptionRequestSchema)
122121
.mutation(async ({ input, ctx }) => {
123122
const { user } = ctx;
@@ -132,20 +131,13 @@ export const organizationsSubscriptionRouter = createTRPCRouter({
132131
const customerId = await getOrCreateStripeCustomerIdForOrganization(org.id);
133132
const subscriptions = await getSubscriptionsForStripeCustomerId(customerId);
134133

135-
// if any subscriptions are not ended, throw bad request error
136134
if (subscriptions.find(sub => sub.ended_at == null)) {
137135
throw new TRPCError({
138136
code: 'BAD_REQUEST',
139137
message: 'Organization has active subscription(s)',
140138
});
141139
}
142140

143-
// if any subscriptions exist we need to enforce security
144-
// otherwise, we can't enforce ownership as the org is still not finished being set up
145-
if (subscriptions.length) {
146-
await ensureOrganizationAccess(ctx, organizationId, ['owner', 'billing_manager']);
147-
}
148-
149141
const result = await getStripeSeatsCheckoutUrl({
150142
kiloUserId: user.id,
151143
stripeCustomerId: customerId,

0 commit comments

Comments
 (0)