@@ -7,7 +7,11 @@ import BillingPlansService from './billing.plans.service.js';
77import SubscriptionRepository from '../repositories/billing.subscription.repository.js' ;
88
99/**
10- * @desc Validate that a URL uses https (or http in dev/test) and belongs to the app domain
10+ * Validate that a redirect URL is safe for the current environment.
11+ * In production the URL must use HTTPS and, when config.domain is set,
12+ * its hostname must match the application domain.
13+ * In development / test only basic URL parsing is enforced (HTTP allowed,
14+ * any hostname accepted) so that localhost workflows are not blocked.
1115 * @param {String } url - URL to validate
1216 * @returns {Boolean } true if valid
1317 */
@@ -40,13 +44,13 @@ const createCheckout = async (organization, priceId, successUrl, cancelUrl) => {
4044 if ( ! stripe ) throw new Error ( 'Stripe is not configured' ) ;
4145
4246 if ( ! isAllowedUrl ( successUrl ) || ! isAllowedUrl ( cancelUrl ) ) {
43- throw new Error ( 'Invalid redirect URL: must use HTTPS and match the application domain' ) ;
47+ throw new Error ( 'Invalid redirect URL: must be a valid URL (production requires HTTPS and a matching application domain) ' ) ;
4448 }
4549
46- // Validate priceId against known active Stripe prices
50+ // Validate priceId against known active Stripe prices and resolve the canonical plan id
4751 const plans = await BillingPlansService . getPlans ( ) ;
48- const allowedPriceIds = plans . flatMap ( ( p ) => [ p . stripePriceMonthly , p . stripePriceAnnual ] . filter ( Boolean ) ) ;
49- if ( ! allowedPriceIds . includes ( priceId ) ) {
52+ const matchedPlan = plans . find ( ( p ) => p . stripePriceMonthly === priceId || p . stripePriceAnnual === priceId ) ;
53+ if ( ! matchedPlan ) {
5054 throw new Error ( 'Invalid priceId: must be an active published price' ) ;
5155 }
5256
@@ -86,9 +90,6 @@ const createCheckout = async (organization, priceId, successUrl, cancelUrl) => {
8690 if ( latest ?. stripeCustomerId ) subscription = latest ;
8791 }
8892
89- // Derive plan name from priceId for checkout metadata
90- const matchedPlan = plans . find ( ( p ) => p . stripePriceMonthly === priceId || p . stripePriceAnnual === priceId ) ;
91-
9293 const session = await stripe . checkout . sessions . create ( {
9394 customer : subscription . stripeCustomerId ,
9495 mode : 'subscription' ,
@@ -97,7 +98,7 @@ const createCheckout = async (organization, priceId, successUrl, cancelUrl) => {
9798 cancel_url : cancelUrl ,
9899 metadata : {
99100 organizationId : String ( organization . _id ) ,
100- plan : matchedPlan ? .planId || 'free' ,
101+ plan : matchedPlan . planId ,
101102 } ,
102103 } ) ;
103104
@@ -119,7 +120,7 @@ const createPortalSession = async (organization, returnUrl) => {
119120
120121 const params = { customer : subscription . stripeCustomerId } ;
121122 if ( returnUrl ) {
122- if ( ! isAllowedUrl ( returnUrl ) ) throw new Error ( 'Invalid return URL: must use HTTPS and match the application domain' ) ;
123+ if ( ! isAllowedUrl ( returnUrl ) ) throw new Error ( 'Invalid return URL: must be a valid URL (production requires HTTPS and a matching application domain) ' ) ;
123124 params . return_url = returnUrl ;
124125 }
125126
0 commit comments