0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-14 13:58:53 +08:00
discourse/plugins/discourse-subscriptions/app/controllers/discourse_subscriptions/subscribe_controller.rb
Isaac Janzen ac2074474f
SECURITY: Subscription contributors expose full user profile fields to anonymous viewers (#40891)
## Summary

Correctly enforce profile visibility restrictions in the subscriptions
contributors endpoint and user serializers. This prevents the exposure
of sensitive profile fields—including bio, location, and website—to
anonymous viewers when public profiles are disabled via site settings.
The fix also hardens the user and user card serializers to omit profile
details for unauthorized scopes as a defense-in-depth measure.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1286

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-06-15 10:24:06 -05:00

307 lines
9.2 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseSubscriptions
class SubscribeController < ::ApplicationController
include DiscourseSubscriptions::Stripe
include DiscourseSubscriptions::Group
requires_plugin PLUGIN_NAME
requires_login except: %i[index contributors show]
def index
product_ids = Product.all.pluck(:external_id)
products = []
if product_ids.present? && is_stripe_configured?
response = ::Stripe::Product.list({ ids: product_ids, active: true }, stripe_request_opts)
products = response[:data].map { |p| serialize_product(p) }
end
render_json_dump products
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
def contributors
return unless SiteSetting.discourse_subscriptions_campaign_show_contributors
guardian.ensure_public_can_see_profiles!
contributor_ids = Set.new
campaign_product = SiteSetting.discourse_subscriptions_campaign_product
if campaign_product.present?
contributor_ids.merge(Customer.where(product_id: campaign_product).last(5).pluck(:user_id))
else
contributor_ids.merge(Customer.last(5).pluck(:user_id))
end
contributors =
::User.where(id: contributor_ids).filter { |user| guardian.can_see_profile?(user) }
render_serialized(contributors, UserSerializer)
end
def show
params.require(:id)
ensure_published_product!(params[:id])
begin
product = ::Stripe::Product.retrieve(params[:id], stripe_request_opts)
plans = ::Stripe::Price.list({ active: true, product: params[:id] }, stripe_request_opts)
response = { product: serialize_product(product), plans: serialize_plans(plans) }
render_json_dump response
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def create
params.require(%i[source plan])
begin
plan = fetch_published_plan(params[:plan])
customer =
find_or_create_customer(
params[:source],
params[:cardholder_name],
params[:cardholder_address],
)
if params[:promo].present?
promo_code = ::Stripe::PromotionCode.list({ code: params[:promo] }, stripe_request_opts)
promo_code = promo_code[:data][0] # we assume promo codes have a unique name
if promo_code.blank?
return render_json_error I18n.t("js.discourse_subscriptions.subscribe.invalid_coupon")
end
end
recurring_plan = plan[:type] == "recurring"
if recurring_plan
trial_days = plan[:metadata][:trial_period_days] if plan[:metadata] &&
plan[:metadata][:trial_period_days]
promo_code_id = promo_code[:id] if promo_code
subscription_params = {
customer: customer[:id],
items: [{ price: params[:plan] }],
metadata: metadata_user,
trial_period_days: trial_days,
promotion_code: promo_code_id,
}
if SiteSetting.discourse_subscriptions_enable_automatic_tax
subscription_params[:automatic_tax] = { enabled: true }
end
transaction = ::Stripe::Subscription.create(subscription_params, stripe_request_opts)
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[
:status
] == "incomplete"
else
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] &&
promo_code[:coupon][:id]
invoice_params = { customer: customer[:id] }
if SiteSetting.discourse_subscriptions_enable_automatic_tax
invoice_params[:automatic_tax] = { enabled: true }
end
invoice = ::Stripe::Invoice.create(invoice_params, stripe_request_opts)
::Stripe::InvoiceItem.create(
{
customer: customer[:id],
price: params[:plan],
discounts: [{ coupon: coupon_id }],
invoice: invoice[:id],
},
stripe_request_opts,
)
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id], {}, stripe_request_opts)
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] ==
"open"
if payment_intent.nil?
return(
render_json_error I18n.t("js.discourse_subscriptions.subscribe.transaction_error")
)
end
transaction =
::Stripe::Invoice.pay(invoice[:id], {}, stripe_request_opts) if payment_intent[
:status
] == "successful"
end
if transaction_ok(transaction)
finalize_transaction(transaction, plan)
else
server_session["pending_subscription"] = {
transaction_id: transaction[:id],
plan_id: plan[:id],
}
end
transaction = transaction.to_h.merge(transaction, payment_intent: payment_intent)
render_json_dump transaction
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def finalize
pending = server_session["pending_subscription"]
raise Discourse::InvalidAccess if pending.blank?
begin
plan = fetch_published_plan(pending[:plan_id])
transaction = retrieve_transaction(pending[:transaction_id])
raise Discourse::InvalidAccess unless transaction_ok(transaction)
finalize_transaction(transaction, plan)
server_session.delete("pending_subscription")
render_json_dump pending[:transaction_id]
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def finalize_transaction(transaction, plan)
group = plan_group(plan)
group.add(current_user) if group
customer =
Customer.create(
user_id: current_user.id,
customer_id: transaction[:customer],
product_id: plan[:product],
)
if transaction[:object] == "subscription"
Subscription.create(
customer_id: customer.id,
external_id: transaction[:id],
status: transaction[:status],
)
end
end
private
def serialize_product(product)
{
id: product[:id],
name: product[:name],
description: PrettyText.cook(product[:metadata][:description]),
subscribed: current_user_products.include?(product[:id]),
repurchaseable: product[:metadata][:repurchaseable],
}
end
def current_user_products
return [] if current_user.nil?
Customer
.joins(:subscriptions)
.where(user_id: current_user.id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.select(:product_id)
.distinct
.pluck(:product_id)
end
def serialize_plans(plans)
plans[:data]
.map { |plan| plan.to_h.slice(:id, :unit_amount, :currency, :type, :recurring) }
.sort_by { |plan| plan[:amount] }
end
def find_or_create_customer(source, cardholder_name = nil, cardholder_address = nil)
customer = Customer.find_by_user_id(current_user.id)
cardholder_address =
(
if cardholder_address.present?
{
line1: cardholder_address[:line1],
city: cardholder_address[:city],
state: cardholder_address[:state],
country: cardholder_address[:country],
postal_code: cardholder_address[:postalCode],
}
else
nil
end
)
if customer.present?
::Stripe::Customer.retrieve(customer.customer_id, stripe_request_opts)
else
::Stripe::Customer.create(
{
email: current_user.email,
source: source,
name: cardholder_name,
address: cardholder_address,
},
stripe_request_opts,
)
end
end
def retrieve_payment_intent(invoice_id)
invoice = ::Stripe::Invoice.retrieve(invoice_id, stripe_request_opts)
::Stripe::PaymentIntent.retrieve(invoice[:payment_intent], stripe_request_opts)
end
def retrieve_transaction(transaction)
case transaction
when /^sub_/
::Stripe::Subscription.retrieve(transaction, stripe_request_opts)
when /^in_/
::Stripe::Invoice.retrieve(transaction, stripe_request_opts)
end
rescue ::Stripe::InvalidRequestError => e
e.message
end
def metadata_user
{ user_id: current_user.id, username: current_user.username_lower }
end
def fetch_published_plan(plan_id)
plan = ::Stripe::Price.retrieve(plan_id, stripe_request_opts)
ensure_published_product!(price_product_id(plan))
plan
end
def ensure_published_product!(product_id)
raise Discourse::NotFound unless Product.exists?(external_id: product_id)
end
def price_product_id(price)
product = price[:product]
return product if product.is_a?(String) || product.nil?
product[:id]
end
def transaction_ok(transaction)
%w[active trialing paid].include?(transaction[:status])
end
end
end