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>
This commit is contained in:
Sojan Jose
2025-08-03 18:41:41 -08:00
co-authored by Claude
parent 647a65d481
commit 602c56d1c5
12 changed files with 404 additions and 5 deletions
+41 -1
View File
@@ -12,6 +12,7 @@
# 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
@@ -19,7 +20,8 @@
#
# Indexes
#
# index_accounts_on_status (status)
# index_accounts_on_sso_config (sso_config) USING gin
# index_accounts_on_status (status)
#
class Account < ApplicationRecord
@@ -55,6 +57,7 @@ class Account < ApplicationRecord
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
@@ -158,6 +161,43 @@ class Account < ApplicationRecord
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
+1 -1
View File
@@ -10,7 +10,7 @@
# enabled :boolean default(TRUE)
# message :text not null
# scheduled_at :datetime
# template_params :jsonb
# template_params :jsonb not null
# title :string not null
# trigger_only_during_business_hours :boolean default(FALSE)
# trigger_rules :jsonb
+20 -1
View File
@@ -2,8 +2,11 @@ module SsoAuthenticatable
extend ActiveSupport::Concern
def generate_sso_auth_token
return nil unless account&.sso_enabled?
token = SecureRandom.hex(32)
::Redis::Alfred.setex(sso_token_key(token), true, 5.minutes)
expiry_minutes = account.sso_token_expiry.minutes
::Redis::Alfred.setex(sso_token_key(token), true, expiry_minutes)
token
end
@@ -16,14 +19,30 @@ module SsoAuthenticatable
end
def generate_sso_link
return nil unless account&.sso_enabled?
encoded_email = ERB::Util.url_encode(email)
"#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token}"
end
def generate_sso_link_with_impersonation
return nil unless account&.sso_enabled?
"#{generate_sso_link}&impersonation=true"
end
def sso_external_login_url
return nil unless account&.sso_enabled?
account.sso_login_url
end
def sso_external_logout_url
return nil unless account&.sso_enabled?
account.sso_logout_url
end
private
def sso_token_key(token)