Files
chatwoot/app/models/account.rb
Sojan JoseandClaude 602c56d1c5 feat: Add account-level SSO configuration
This implements Single Sign-On (SSO) functionality at the account level, allowing individual accounts to configure their own SSO settings.

Key changes:
- Added SSO configuration to account model with JSONB storage
- Implemented SSO settings UI in account settings panel
- Added feature flag to control SSO access (premium feature)
- Updated authentication flow to support account-level SSO
- Added proper validation and error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-03 18:41:41 -08:00

229 lines
8.3 KiB
Ruby

# == Schema Information
#
# Table name: accounts
#
# id :integer not null, primary key
# auto_resolve_duration :integer
# custom_attributes :jsonb
# domain :string(100)
# feature_flags :bigint default(0), not null
# internal_attributes :jsonb not null
# limits :jsonb
# locale :integer default("en")
# name :string not null
# settings :jsonb
# sso_config :jsonb not null
# status :integer default("active")
# support_email :string(100)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_accounts_on_sso_config (sso_config) USING gin
# index_accounts_on_status (status)
#
class Account < ApplicationRecord
# used for single column multi flags
include FlagShihTzu
include Reportable
include Featurable
include CacheKeys
SETTINGS_PARAMS_SCHEMA = {
'type': 'object',
'properties':
{
'auto_resolve_after': { 'type': %w[integer null], 'minimum': 10, 'maximum': 1_439_856 },
'auto_resolve_message': { 'type': %w[string null] },
'auto_resolve_ignore_waiting': { 'type': %w[boolean null] },
'audio_transcriptions': { 'type': %w[boolean null] },
'auto_resolve_label': { 'type': %w[string null] }
},
'required': [],
'additionalProperties': true
}.to_json.freeze
DEFAULT_QUERY_SETTING = {
flag_query_mode: :bit_operator,
check_for_column: false
}.freeze
validates :domain, length: { maximum: 100 }
validates_with JsonSchemaValidator,
schema: SETTINGS_PARAMS_SCHEMA,
attribute_resolver: ->(record) { record.settings }
store_accessor :settings, :auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :sso_config, :enabled, :provider_name, :login_url, :logout_url, :secret_key, :token_expiry
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async
has_many :agent_bots, dependent: :destroy_async
has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api'
has_many :articles, dependent: :destroy_async, class_name: '::Article'
has_many :automation_rules, dependent: :destroy_async
has_many :macros, dependent: :destroy_async
has_many :campaigns, dependent: :destroy_async
has_many :canned_responses, dependent: :destroy_async
has_many :categories, dependent: :destroy_async, class_name: '::Category'
has_many :contacts, dependent: :destroy_async
has_many :conversations, dependent: :destroy_async
has_many :csat_survey_responses, dependent: :destroy_async
has_many :custom_attribute_definitions, dependent: :destroy_async
has_many :custom_filters, dependent: :destroy_async
has_many :dashboard_apps, dependent: :destroy_async
has_many :data_imports, dependent: :destroy_async
has_many :email_channels, dependent: :destroy_async, class_name: '::Channel::Email'
has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage'
has_many :instagram_channels, dependent: :destroy_async, class_name: '::Channel::Instagram'
has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook'
has_many :inboxes, dependent: :destroy_async
has_many :labels, dependent: :destroy_async
has_many :line_channels, dependent: :destroy_async, class_name: '::Channel::Line'
has_many :mentions, dependent: :destroy_async
has_many :messages, dependent: :destroy_async
has_many :notes, dependent: :destroy_async
has_many :notification_settings, dependent: :destroy_async
has_many :notifications, dependent: :destroy_async
has_many :portals, dependent: :destroy_async, class_name: '::Portal'
has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
has_many :teams, dependent: :destroy_async
has_many :telegram_bots, dependent: :destroy_async
has_many :telegram_channels, dependent: :destroy_async, class_name: '::Channel::Telegram'
has_many :twilio_sms, dependent: :destroy_async, class_name: '::Channel::TwilioSms'
has_many :twitter_profiles, dependent: :destroy_async, class_name: '::Channel::TwitterProfile'
has_many :users, through: :account_users
has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
has_many :webhooks, dependent: :destroy_async
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
has_many :working_hours, dependent: :destroy_async
has_one_attached :contacts_export
enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true
enum :status, { active: 0, suspended: 1 }
scope :with_auto_resolve, -> { where("(settings ->> 'auto_resolve_after')::int IS NOT NULL") }
before_validation :validate_limit_keys
after_create_commit :notify_creation
after_destroy :remove_account_sequences
def agents
users.where(account_users: { role: :agent })
end
def administrators
users.where(account_users: { role: :administrator })
end
def all_conversation_tags
# returns array of tags
conversation_ids = conversations.pluck(:id)
ActsAsTaggableOn::Tagging.includes(:tag)
.where(context: 'labels',
taggable_type: 'Conversation',
taggable_id: conversation_ids)
.map { |tagging| tagging.tag.name }
end
def webhook_data
{
id: id,
name: name
}
end
def inbound_email_domain
domain.presence || GlobalConfig.get('MAILER_INBOUND_EMAIL_DOMAIN')['MAILER_INBOUND_EMAIL_DOMAIN'] || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN',
false)
end
def support_email
super.presence || ENV.fetch('MAILER_SENDER_EMAIL') { GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] }
end
def usage_limits
{
agents: ChatwootApp.max_limit.to_i,
inboxes: ChatwootApp.max_limit.to_i
}
end
def locale_english_name
# the locale can also be something like pt_BR, en_US, fr_FR, etc.
# the format is `<locale_code>_<country_code>`
# we need to extract the language code from the locale
account_locale = locale&.split('_')&.first
ISO_639.find(account_locale)&.english_name&.downcase || 'english'
end
# SSO Configuration Methods
def sso_enabled?
ActiveModel::Type::Boolean.new.cast(sso_config['enabled'])
end
def sso_provider_name
sso_config['provider_name'].presence || 'SSO'
end
def sso_login_url
sso_config['login_url']
end
def sso_logout_url
sso_config['logout_url']
end
def sso_secret_key
sso_config['secret_key']
end
def sso_token_expiry
(sso_config['token_expiry'].presence || 5).to_i
end
def update_sso_config(config)
# Validate required fields if SSO is enabled
return false if ActiveModel::Type::Boolean.new.cast(config['enabled']) && (config['login_url'].blank? || config['secret_key'].blank?)
# Set default values
config['token_expiry'] = 5 if config['token_expiry'].blank?
config['provider_name'] = 'SSO' if config['provider_name'].blank?
self.sso_config = config
save
end
private
def notify_creation
Rails.configuration.dispatcher.dispatch(ACCOUNT_CREATED, Time.zone.now, account: self)
end
trigger.after(:insert).for_each(:row) do
"execute format('create sequence IF NOT EXISTS conv_dpid_seq_%s', NEW.id);"
end
trigger.name('camp_dpid_before_insert').after(:insert).for_each(:row) do
"execute format('create sequence IF NOT EXISTS camp_dpid_seq_%s', NEW.id);"
end
def validate_limit_keys
# method overridden in enterprise module
end
def remove_account_sequences
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS camp_dpid_seq_#{id}")
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS conv_dpid_seq_#{id}")
end
end
Account.prepend_mod_with('Account')
Account.prepend_mod_with('Account::PlanUsageAndLimits')
Account.include_mod_with('Concerns::Account')
Account.include_mod_with('Audit::Account')