This change blocks Help Center access for default/Hacker-plan accounts and closes the downgrade gap that could leave `help_center` enabled after a subscription falls back to the default cloud plan. Fixes: none Closes: none ## Why Default-plan accounts should not be able to access the Help Center, but the downgrade fallback path only reset the plan name and did not reconcile premium feature flags. That meant some accounts could keep `help_center` enabled even after landing back on the Hacker/default plan. ## What this change does - blocks Help Center portal and article access for default/Hacker-plan accounts - reconciles premium feature flags when a subscription falls back to the default cloud plan, so `help_center` is disabled immediately instead of waiting for a later webhook - preserves existing account `custom_attributes` during Stripe customer recreation instead of overwriting them - adds Enterprise coverage for the default-plan access checks on hosted and custom-domain Help Center routes - fixes the public access check to use the resolved portal object so blocked requests return the intended response instead of raising an error ## Validation 1. Create or use an account on the default/Hacker cloud plan with an active portal. 2. Visit the portal home page and a published article on both the Chatwoot-hosted URL and a configured custom domain. 3. Confirm the Help Center is blocked for that account. 4. Downgrade a paid account back to the default/Hacker plan through the Stripe webhook flow. 5. Confirm `help_center` is disabled right after the downgrade fallback is processed and the account can no longer access the Help Center. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
93 lines
2.8 KiB
Ruby
93 lines
2.8 KiB
Ruby
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
|
before_action :portal
|
|
before_action :ensure_portal_feature_enabled
|
|
before_action :set_category, except: [:index, :show, :tracking_pixel]
|
|
before_action :set_article, only: [:show]
|
|
layout 'portal'
|
|
|
|
def index
|
|
@search_query = list_params[:query]
|
|
@articles = @portal.articles.published.includes(:category, :author)
|
|
|
|
@articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present?
|
|
|
|
@articles_count = @articles.count
|
|
|
|
search_articles
|
|
order_by_sort_param
|
|
limit_results
|
|
end
|
|
|
|
def show
|
|
@og_image_url = helpers.set_og_image_url(@portal.name, @article.title)
|
|
end
|
|
|
|
def tracking_pixel
|
|
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
|
return head :not_found unless @article
|
|
|
|
@article.increment_view_count if @article.published?
|
|
|
|
# Serve the 1x1 tracking pixel with 24-hour private cache
|
|
# Private cache bypasses CDN but allows browser caching to prevent duplicate views from same user
|
|
expires_in 24.hours, public: false
|
|
response.headers['Content-Type'] = 'image/png'
|
|
|
|
pixel_path = Rails.public_path.join('assets/images/tracking-pixel.png')
|
|
send_file pixel_path, type: 'image/png', disposition: 'inline'
|
|
end
|
|
|
|
private
|
|
|
|
def limit_results
|
|
return if list_params[:per_page].blank?
|
|
|
|
per_page = [list_params[:per_page].to_i, 100].min
|
|
per_page = 25 if per_page < 1
|
|
@articles = @articles.page(list_params[:page]).per(per_page)
|
|
end
|
|
|
|
def search_articles
|
|
@articles = @articles.search(list_params) if list_params.present?
|
|
end
|
|
|
|
def order_by_sort_param
|
|
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
|
|
@articles.order_by_views
|
|
else
|
|
@articles.order_by_position
|
|
end
|
|
end
|
|
|
|
def set_article
|
|
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
|
@parsed_content = render_article_content(@article.content)
|
|
end
|
|
|
|
def set_category
|
|
return if permitted_params[:category_slug].blank?
|
|
|
|
@category = @portal.categories.find_by!(
|
|
slug: permitted_params[:category_slug],
|
|
locale: permitted_params[:locale]
|
|
)
|
|
end
|
|
|
|
def list_params
|
|
@list_params ||= params.permit(:query, :locale, :sort, :status, :page, :per_page).tap do |permitted|
|
|
permitted[:query] = permitted[:query].to_s.strip.presence
|
|
end
|
|
end
|
|
|
|
def permitted_params
|
|
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
|
|
end
|
|
|
|
def render_article_content(content)
|
|
ChatwootMarkdownRenderer.new(content).render_article
|
|
end
|
|
end
|
|
|
|
Public::Api::V1::Portals::ArticlesController.prepend_mod_with('Public::Api::V1::Portals::ArticlesController')
|