Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
module Enterprise::Api::V1::Accounts::AgentsController
|
||||
def create
|
||||
super
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
def update
|
||||
super
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def associate_agent_with_custom_role
|
||||
@agent.current_account_user.update!(custom_role_id: params[:custom_role_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
module Enterprise::Api::V1::Accounts::ConversationsController
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def inbox_assistant
|
||||
assistant = @conversation.inbox.captain_assistant
|
||||
|
||||
if assistant
|
||||
render json: { assistant: { id: assistant.id, name: assistant.name } }
|
||||
else
|
||||
render json: { assistant: nil }
|
||||
end
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events = @conversation.reporting_events.order(created_at: :asc)
|
||||
end
|
||||
|
||||
def permitted_update_params
|
||||
super.merge(params.permit(:sla_policy_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def copilot_params
|
||||
params.permit(:previous_history, :message, :assistant_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
module Enterprise::Api::V1::Accounts::CsatSurveyResponsesController
|
||||
def update
|
||||
@csat_survey_response = Current.account.csat_survey_responses.find(params[:id])
|
||||
authorize @csat_survey_response
|
||||
|
||||
@csat_survey_response.update!(
|
||||
csat_review_notes: params[:csat_review_notes],
|
||||
review_notes_updated_by: Current.user,
|
||||
review_notes_updated_at: Time.current
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
module Enterprise::Api::V1::Accounts::InboxesController
|
||||
def inbox_attributes
|
||||
super + ee_inbox_attributes
|
||||
end
|
||||
|
||||
def ee_inbox_attributes
|
||||
[auto_assignment_config: [:max_assignment_limit]]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_channel_types
|
||||
super + ['voice']
|
||||
end
|
||||
|
||||
def channel_type_from_params
|
||||
case permitted_params[:channel][:type]
|
||||
when 'voice'
|
||||
Channel::Voice
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def account_channels_method
|
||||
case permitted_params[:channel][:type]
|
||||
when 'voice'
|
||||
Current.account.voice_channels
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module Enterprise::Api::V1::Accounts::PortalsController
|
||||
def ssl_status
|
||||
return render_could_not_create_error(I18n.t('portals.ssl_status.custom_domain_not_configured')) if @portal.custom_domain.blank?
|
||||
|
||||
result = Cloudflare::CheckCustomHostnameService.new(portal: @portal).perform
|
||||
|
||||
return render_could_not_create_error(result[:errors]) if result[:errors].present?
|
||||
|
||||
ssl_settings = @portal.ssl_settings || {}
|
||||
render json: {
|
||||
status: ssl_settings['cf_status'],
|
||||
verification_errors: ssl_settings['cf_verification_errors']
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,147 @@
|
||||
class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
include BillingHelper
|
||||
before_action :fetch_account
|
||||
before_action :check_authorization
|
||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion]
|
||||
|
||||
def subscription
|
||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||
@account.update(custom_attributes: { is_creating_customer: true })
|
||||
Enterprise::CreateStripeCustomerJob.perform_later(@account)
|
||||
end
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def limits
|
||||
limits = if default_plan?(@account)
|
||||
{
|
||||
'conversation' => {
|
||||
'allowed' => 500,
|
||||
'consumed' => conversations_this_month(@account)
|
||||
},
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => non_web_inboxes(@account)
|
||||
},
|
||||
'agents' => {
|
||||
'allowed' => 2,
|
||||
'consumed' => agents(@account)
|
||||
}
|
||||
}
|
||||
else
|
||||
default_limits
|
||||
end
|
||||
|
||||
# include id in response to ensure that the store can be updated on the frontend
|
||||
render json: { id: @account.id, limits: limits }, status: :ok
|
||||
end
|
||||
|
||||
def checkout
|
||||
return create_stripe_billing_session(stripe_customer_id) if stripe_customer_id.present?
|
||||
|
||||
render_invalid_billing_details
|
||||
end
|
||||
|
||||
def toggle_deletion
|
||||
action_type = params[:action_type]
|
||||
|
||||
case action_type
|
||||
when 'delete'
|
||||
mark_for_deletion
|
||||
when 'undelete'
|
||||
unmark_for_deletion
|
||||
else
|
||||
render json: { error: 'Invalid action_type. Must be either "delete" or "undelete"' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def topup_checkout
|
||||
return render json: { error: I18n.t('errors.topup.credits_required') }, status: :unprocessable_entity if params[:credits].blank?
|
||||
|
||||
service = Enterprise::Billing::TopupCheckoutService.new(account: @account)
|
||||
result = service.create_checkout_session(credits: params[:credits].to_i)
|
||||
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
limits: @account.limits,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
rescue Enterprise::Billing::TopupCheckoutService::Error, Stripe::StripeError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_cloud_env
|
||||
render json: { error: 'Not found' }, status: :not_found unless ChatwootApp.chatwoot_cloud?
|
||||
end
|
||||
|
||||
def default_limits
|
||||
{
|
||||
'conversation' => {},
|
||||
'non_web_inboxes' => {},
|
||||
'agents' => {
|
||||
'allowed' => @account.usage_limits[:agents],
|
||||
'consumed' => agents(@account)
|
||||
},
|
||||
'captain' => @account.usage_limits[:captain]
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def stripe_customer_id
|
||||
@account.custom_attributes['stripe_customer_id']
|
||||
end
|
||||
|
||||
def mark_for_deletion
|
||||
reason = 'manual_deletion'
|
||||
|
||||
if @account.mark_for_deletion(reason)
|
||||
cancel_cloud_subscriptions_for_deletion
|
||||
|
||||
render json: { message: 'Account marked for deletion' }, status: :ok
|
||||
else
|
||||
render json: { message: @account.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def unmark_for_deletion
|
||||
if @account.unmark_for_deletion
|
||||
render json: { message: 'Account unmarked for deletion' }, status: :ok
|
||||
else
|
||||
render json: { message: @account.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def render_invalid_billing_details
|
||||
render_could_not_create_error('Please subscribe to a plan before viewing the billing details')
|
||||
end
|
||||
|
||||
def create_stripe_billing_session(customer_id)
|
||||
session = Enterprise::Billing::CreateSessionService.new.create_session(customer_id)
|
||||
render_redirect_url(session.url)
|
||||
end
|
||||
|
||||
def cancel_cloud_subscriptions_for_deletion
|
||||
Enterprise::Billing::CancelCloudSubscriptionsService.new(account: @account).perform
|
||||
rescue Stripe::StripeError => e
|
||||
Rails.logger.warn("Failed to cancel cloud subscriptions for account #{@account.id}: #{e.class} - #{e.message}")
|
||||
end
|
||||
|
||||
def render_redirect_url(redirect_url)
|
||||
render json: { redirect_url: redirect_url }
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: current_user,
|
||||
account: @account,
|
||||
account_user: @current_account_user
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
module Enterprise::Api::V1::AccountsSettings
|
||||
private
|
||||
|
||||
def permitted_settings_attributes
|
||||
super + [{ conversation_required_attributes: [] }]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module Enterprise::Api::V2::AccountsController
|
||||
private
|
||||
|
||||
def fetch_account_and_user_info
|
||||
@data = fetch_from_clearbit
|
||||
|
||||
return if @data.blank?
|
||||
|
||||
update_user_info
|
||||
end
|
||||
|
||||
def fetch_from_clearbit
|
||||
Enterprise::ClearbitLookupService.lookup(@user.email)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error fetching data from clearbit: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
def update_user_info
|
||||
@user.update!(name: @data[:name]) if @data[:name].present?
|
||||
end
|
||||
|
||||
def data_from_clearbit
|
||||
return {} if @data.blank?
|
||||
|
||||
{ name: @data[:company_name],
|
||||
custom_attributes: {
|
||||
'industry' => @data[:industry],
|
||||
'company_size' => @data[:company_size],
|
||||
'timezone' => @data[:timezone],
|
||||
'logo' => @data[:logo]
|
||||
} }
|
||||
end
|
||||
|
||||
def account_attributes
|
||||
super.deep_merge(
|
||||
data_from_clearbit
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module Enterprise::Concerns::ApplicationControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
end
|
||||
@@ -0,0 +1,100 @@
|
||||
module Enterprise::DeviseOverrides::OmniauthCallbacksController
|
||||
def saml
|
||||
# Call parent's omniauth_success which handles the auth
|
||||
omniauth_success
|
||||
end
|
||||
|
||||
def redirect_callbacks
|
||||
# derive target redirect route from 'resource_class' param, which was set
|
||||
# before authentication.
|
||||
devise_mapping = get_devise_mapping
|
||||
redirect_route = get_redirect_route(devise_mapping)
|
||||
|
||||
# preserve omniauth info for success route. ignore 'extra' in twitter
|
||||
# auth response to avoid CookieOverflow.
|
||||
session['dta.omniauth.auth'] = request.env['omniauth.auth'].except('extra')
|
||||
session['dta.omniauth.params'] = request.env['omniauth.params']
|
||||
|
||||
# For SAML, use 303 See Other to convert POST to GET and preserve session
|
||||
if params[:provider] == 'saml'
|
||||
redirect_to redirect_route, { status: 303 }.merge(redirect_options)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def omniauth_success
|
||||
case auth_hash&.dig('provider')
|
||||
when 'saml'
|
||||
handle_saml_auth
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def omniauth_failure
|
||||
return super unless params[:provider] == 'saml'
|
||||
|
||||
relay_state = saml_relay_state
|
||||
error = params[:message] || 'authentication-failed'
|
||||
|
||||
if for_mobile?(relay_state)
|
||||
redirect_to_mobile_error(error, relay_state)
|
||||
else
|
||||
redirect_to login_page_url(error: "saml-#{error}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_saml_auth
|
||||
account_id = extract_saml_account_id
|
||||
relay_state = saml_relay_state
|
||||
|
||||
unless saml_enabled_for_account?(account_id)
|
||||
return redirect_to_mobile_error('saml-not-enabled') if for_mobile?(relay_state)
|
||||
|
||||
return redirect_to login_page_url(error: 'saml-not-enabled')
|
||||
end
|
||||
|
||||
@resource = SamlUserBuilder.new(auth_hash, account_id).perform
|
||||
|
||||
if @resource.persisted?
|
||||
return sign_in_user_on_mobile if for_mobile?(relay_state)
|
||||
|
||||
sign_in_user
|
||||
else
|
||||
return redirect_to_mobile_error('saml-authentication-failed') if for_mobile?(relay_state)
|
||||
|
||||
redirect_to login_page_url(error: 'saml-authentication-failed')
|
||||
end
|
||||
end
|
||||
|
||||
def extract_saml_account_id
|
||||
params[:account_id] || session[:saml_account_id] || request.env['omniauth.params']&.dig('account_id')
|
||||
end
|
||||
|
||||
def saml_relay_state
|
||||
session[:saml_relay_state] || request.env['omniauth.params']&.dig('RelayState')
|
||||
end
|
||||
|
||||
def for_mobile?(relay_state)
|
||||
relay_state.to_s.casecmp('mobile').zero?
|
||||
end
|
||||
|
||||
def redirect_to_mobile_error(error)
|
||||
mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp')
|
||||
redirect_to "#{mobile_deep_link_base}://auth/saml?error=#{ERB::Util.url_encode(error)}", allow_other_host: true
|
||||
end
|
||||
|
||||
def saml_enabled_for_account?(account_id)
|
||||
return false if account_id.blank?
|
||||
|
||||
account = Account.find_by(id: account_id)
|
||||
|
||||
return false if account.nil?
|
||||
return false unless account.feature_enabled?('saml')
|
||||
|
||||
AccountSamlSettings.find_by(account_id: account_id).present?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
module Enterprise::DeviseOverrides::PasswordsController
|
||||
include SamlAuthenticationHelper
|
||||
|
||||
def create
|
||||
if saml_user_attempting_password_auth?(params[:email])
|
||||
render json: {
|
||||
success: false,
|
||||
message: I18n.t('messages.reset_password_saml_user'),
|
||||
errors: [I18n.t('messages.reset_password_saml_user')]
|
||||
}, status: :forbidden
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module Enterprise::DeviseOverrides::SessionsController
|
||||
include SamlAuthenticationHelper
|
||||
|
||||
def create
|
||||
if saml_user_attempting_password_auth?(params[:email], sso_auth_token: params[:sso_auth_token])
|
||||
render json: {
|
||||
success: false,
|
||||
message: I18n.t('messages.login_saml_user'),
|
||||
errors: [I18n.t('messages.login_saml_user')]
|
||||
}, status: :unauthorized
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def render_create_success
|
||||
create_audit_event('sign_in')
|
||||
super
|
||||
end
|
||||
|
||||
def destroy
|
||||
create_audit_event('sign_out')
|
||||
super
|
||||
end
|
||||
|
||||
def create_audit_event(action)
|
||||
return unless @resource
|
||||
|
||||
associated_type = 'Account'
|
||||
@resource.accounts.each do |account|
|
||||
@resource.audits.create(
|
||||
action: action,
|
||||
user_id: @resource.id,
|
||||
associated_id: account.id,
|
||||
associated_type: associated_type
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
module Enterprise::Public::Api::V1::Portals::ArticlesController
|
||||
private
|
||||
|
||||
def search_articles
|
||||
if @portal.account.feature_enabled?('help_center_embedding_search')
|
||||
@articles = @articles.vector_search(list_params.merge(account_id: @portal.account_id)) if list_params[:query].present?
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module Enterprise::SuperAdmin::AccountsController
|
||||
def update
|
||||
# Handle manually managed features from form submission
|
||||
if params[:account] && params[:account][:manually_managed_features].present?
|
||||
# Update using the service - it will handle array conversion and validation
|
||||
service = ::Internal::Accounts::InternalAttributesService.new(requested_resource)
|
||||
service.manually_managed_features = params[:account][:manually_managed_features]
|
||||
|
||||
# Remove the manually_managed_features from params to prevent ActiveModel::UnknownAttributeError
|
||||
params[:account].delete(:manually_managed_features)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
module Enterprise::SuperAdmin::AppConfigsController
|
||||
private
|
||||
|
||||
def allowed_configs
|
||||
return super if ChatwootHub.pricing_plan == 'community'
|
||||
|
||||
case @config
|
||||
when 'custom_branding'
|
||||
@allowed_configs = custom_branding_options
|
||||
when 'internal'
|
||||
@allowed_configs = internal_config_options
|
||||
when 'captain'
|
||||
@allowed_configs = captain_config_options
|
||||
when 'saml'
|
||||
@allowed_configs = saml_config_options
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def custom_branding_options
|
||||
%w[
|
||||
LOGO_THUMBNAIL
|
||||
LOGO
|
||||
LOGO_DARK
|
||||
BRAND_NAME
|
||||
INSTALLATION_NAME
|
||||
BRAND_URL
|
||||
WIDGET_BRAND_URL
|
||||
TERMS_URL
|
||||
PRIVACY_URL
|
||||
DISPLAY_MANIFEST
|
||||
]
|
||||
end
|
||||
|
||||
def internal_config_options
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY CLOUD_ANALYTICS_TOKEN CLEARBIT_API_KEY DASHBOARD_SCRIPTS INACTIVE_WHATSAPP_NUMBERS
|
||||
SKIP_INCOMING_BCC_PROCESSING CAPTAIN_CLOUD_PLAN_LIMITS ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL CHATWOOT_INSTANCE_ADMIN_EMAIL
|
||||
OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF CLOUDFLARE_API_KEY CLOUDFLARE_ZONE_ID BLOCKED_EMAIL_DOMAINS
|
||||
OTEL_PROVIDER LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY LANGFUSE_BASE_URL]
|
||||
end
|
||||
|
||||
def captain_config_options
|
||||
%w[
|
||||
CAPTAIN_OPEN_AI_API_KEY
|
||||
CAPTAIN_OPEN_AI_MODEL
|
||||
CAPTAIN_OPEN_AI_ENDPOINT
|
||||
CAPTAIN_EMBEDDING_MODEL
|
||||
CAPTAIN_FIRECRAWL_API_KEY
|
||||
]
|
||||
end
|
||||
|
||||
def saml_config_options
|
||||
%w[ENABLE_SAML_SSO_LOGIN]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
class Enterprise::Webhooks::FirecrawlController < ActionController::API
|
||||
before_action :validate_token
|
||||
|
||||
def process_payload
|
||||
Captain::Tools::FirecrawlParserJob.perform_later(assistant_id: assistant.id, payload: payload) if crawl_page_event?
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
include Captain::FirecrawlHelper
|
||||
|
||||
def payload
|
||||
permitted_params[:data]&.first&.to_h
|
||||
end
|
||||
|
||||
def validate_token
|
||||
render json: { error: 'Invalid access_token' }, status: :unauthorized if assistant_token != permitted_params[:token]
|
||||
end
|
||||
|
||||
def assistant
|
||||
@assistant ||= Captain::Assistant.find(permitted_params[:assistant_id])
|
||||
end
|
||||
|
||||
def assistant_token
|
||||
generate_firecrawl_token(assistant.id, assistant.account_id)
|
||||
end
|
||||
|
||||
def crawl_page_event?
|
||||
permitted_params[:type] == 'crawl.page'
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:type,
|
||||
:assistant_id,
|
||||
:token,
|
||||
:success,
|
||||
:id,
|
||||
:metadata,
|
||||
:format,
|
||||
:firecrawl,
|
||||
data: [:markdown, { metadata: {} }]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class Enterprise::Webhooks::StripeController < ActionController::API
|
||||
def process_payload
|
||||
# Get the event payload and signature
|
||||
payload = request.body.read
|
||||
sig_header = request.headers['Stripe-Signature']
|
||||
|
||||
# Attempt to verify the signature. If successful, we'll handle the event
|
||||
begin
|
||||
event = Stripe::Webhook.construct_event(payload, sig_header, ENV.fetch('STRIPE_WEBHOOK_SECRET', nil))
|
||||
::Enterprise::Billing::HandleStripeEventService.new.perform(event: event)
|
||||
# If we fail to verify the signature, then something was wrong with the request
|
||||
rescue JSON::ParserError, Stripe::SignatureVerificationError
|
||||
# Invalid payload
|
||||
head :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
# We've successfully processed the event without blowing up
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module Enterprise::WidgetsController
|
||||
private
|
||||
|
||||
def ensure_location_is_supported
|
||||
countries = @web_widget.inbox.account.custom_attributes['allowed_countries']
|
||||
return if countries.blank?
|
||||
|
||||
geocoder_result = IpLookupService.new.perform(request.remote_ip)
|
||||
return unless geocoder_result
|
||||
|
||||
country_enabled = countries.include?(geocoder_result.country_code)
|
||||
render json: { error: 'Location is not supported' }, status: :unauthorized unless country_enabled
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user