Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602c56d1c5 |
@@ -47,6 +47,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.settings.merge!(settings_params)
|
||||
@account.sso_config.merge!(sso_config_params) if sso_config_params.present? && sso_feature_enabled?
|
||||
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
|
||||
@account.save!
|
||||
end
|
||||
@@ -95,6 +96,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
|
||||
end
|
||||
|
||||
def sso_config_params
|
||||
params.permit(sso_config: [:enabled, :provider_name, :login_url, :logout_url, :secret_key, :token_expiry])[:sso_config]
|
||||
end
|
||||
|
||||
def sso_feature_enabled?
|
||||
@account.feature_enabled?('sso')
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
@@ -39,6 +39,7 @@ export const FEATURE_FLAGS = {
|
||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||
WHATSAPP_EMBEDDED_SIGNUP: 'whatsapp_embedded_signup',
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
SSO: 'sso',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURES = [
|
||||
@@ -48,4 +49,5 @@ export const PREMIUM_FEATURES = [
|
||||
FEATURE_FLAGS.AUDIT_LOGS,
|
||||
FEATURE_FLAGS.HELP_CENTER,
|
||||
FEATURE_FLAGS.CAPTAIN_V2,
|
||||
FEATURE_FLAGS.SSO,
|
||||
];
|
||||
|
||||
@@ -123,6 +123,50 @@
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
|
||||
}
|
||||
},
|
||||
"SSO": {
|
||||
"TITLE": "Single Sign-On",
|
||||
"DESCRIPTION": "Configure SSO authentication for your organization",
|
||||
"FORM": {
|
||||
"ENABLE_SSO": {
|
||||
"LABEL": "Enable SSO",
|
||||
"HELP": "Allow users to log in using Single Sign-On"
|
||||
},
|
||||
"PROVIDER_NAME": {
|
||||
"LABEL": "Provider Name",
|
||||
"PLACEHOLDER": "e.g., 'Okta', 'Azure AD', 'Google Workspace'",
|
||||
"ERROR": "Please enter a provider name"
|
||||
},
|
||||
"LOGIN_URL": {
|
||||
"LABEL": "SSO Login URL",
|
||||
"PLACEHOLDER": "https://your-sso-provider.com/login",
|
||||
"ERROR": "Please enter a valid SSO login URL",
|
||||
"HELP": "Users will be redirected to this URL for authentication"
|
||||
},
|
||||
"LOGOUT_URL": {
|
||||
"LABEL": "SSO Logout URL (Optional)",
|
||||
"PLACEHOLDER": "https://your-sso-provider.com/logout",
|
||||
"HELP": "Users will be redirected to this URL after logout"
|
||||
},
|
||||
"SECRET_KEY": {
|
||||
"LABEL": "Secret Key",
|
||||
"PLACEHOLDER": "Enter your SSO secret key",
|
||||
"ERROR": "Please enter a secret key",
|
||||
"HELP": "Secret key used to verify SSO tokens"
|
||||
},
|
||||
"TOKEN_EXPIRY": {
|
||||
"LABEL": "Token Expiry (minutes)",
|
||||
"PLACEHOLDER": "5",
|
||||
"ERROR": "Please enter a valid expiry time",
|
||||
"HELP": "How long SSO tokens remain valid"
|
||||
},
|
||||
"ERROR": "Please fix the form errors"
|
||||
},
|
||||
"SUBMIT": "Update SSO Settings",
|
||||
"UPDATE": {
|
||||
"SUCCESS": "SSO settings updated successfully",
|
||||
"ERROR": "Failed to update SSO settings"
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
|
||||
"LEARN_MORE": "Learn more",
|
||||
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"BUSINESS_ACCOUNTS_ONLY": "Please use your company email address to login",
|
||||
"NO_ACCOUNT_FOUND": "We couldn't find an account for your email address."
|
||||
},
|
||||
"SSO": {
|
||||
"BUTTON_TEXT": "Login with {provider}"
|
||||
},
|
||||
"FORGOT_PASSWORD": "Forgot your password?",
|
||||
"CREATE_NEW_ACCOUNT": "Create a new account",
|
||||
"SUBMIT": "Login"
|
||||
|
||||
@@ -17,6 +17,7 @@ import BuildInfo from './components/BuildInfo.vue';
|
||||
import AccountDelete from './components/AccountDelete.vue';
|
||||
import AutoResolve from './components/AutoResolve.vue';
|
||||
import AudioTranscription from './components/AudioTranscription.vue';
|
||||
import SSOConfiguration from './components/SSOConfiguration.vue';
|
||||
import SectionLayout from './components/SectionLayout.vue';
|
||||
|
||||
export default {
|
||||
@@ -28,6 +29,7 @@ export default {
|
||||
AccountDelete,
|
||||
AutoResolve,
|
||||
AudioTranscription,
|
||||
SSOConfiguration,
|
||||
SectionLayout,
|
||||
WithLabel,
|
||||
NextInput,
|
||||
@@ -77,6 +79,9 @@ export default {
|
||||
FEATURE_FLAGS.CAPTAIN
|
||||
);
|
||||
},
|
||||
showSSOConfig() {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SSO);
|
||||
},
|
||||
languagesSortedByCode() {
|
||||
const enabledLanguages = [...this.enabledLanguages];
|
||||
return enabledLanguages.sort((l1, l2) =>
|
||||
@@ -244,6 +249,7 @@ export default {
|
||||
</div>
|
||||
<AutoResolve v-if="showAutoResolutionConfig" />
|
||||
<AudioTranscription v-if="showAudioTranscriptionConfig" />
|
||||
<SSOConfiguration v-if="showSSOConfig" />
|
||||
<AccountId />
|
||||
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
|
||||
<AccountDelete />
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, url } from '@vuelidate/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import NextInput from 'next/input/Input.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import SectionLayout from './SectionLayout.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
NextInput,
|
||||
NextButton,
|
||||
SectionLayout,
|
||||
},
|
||||
setup() {
|
||||
const { accountId } = useAccount();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
return { accountId, v$ };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ssoConfig: {
|
||||
enabled: false,
|
||||
provider_name: 'SSO',
|
||||
login_url: '',
|
||||
logout_url: '',
|
||||
secret_key: '',
|
||||
token_expiry: 5,
|
||||
},
|
||||
showSecretKey: false,
|
||||
isUpdating: false,
|
||||
};
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
ssoConfig: {
|
||||
provider_name: {
|
||||
required,
|
||||
},
|
||||
login_url: {
|
||||
required: this.ssoConfig.enabled ? required : true,
|
||||
url: this.ssoConfig.enabled ? url : true,
|
||||
},
|
||||
secret_key: {
|
||||
required: this.ssoConfig.enabled ? required : true,
|
||||
},
|
||||
token_expiry: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
}),
|
||||
currentAccount() {
|
||||
return this.getAccount(this.accountId) || {};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initializeSSO();
|
||||
},
|
||||
methods: {
|
||||
initializeSSO() {
|
||||
try {
|
||||
const { sso_config } = this.currentAccount;
|
||||
if (sso_config) {
|
||||
this.ssoConfig = {
|
||||
enabled: sso_config.enabled || false,
|
||||
provider_name: sso_config.provider_name || 'SSO',
|
||||
login_url: sso_config.login_url || '',
|
||||
logout_url: sso_config.logout_url || '',
|
||||
secret_key: sso_config.secret_key || '',
|
||||
token_expiry: sso_config.token_expiry || 5,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
|
||||
async updateSSO() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
useAlert(this.$t('GENERAL_SETTINGS.SSO.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('accounts/update', {
|
||||
sso_config: this.ssoConfig,
|
||||
});
|
||||
useAlert(this.$t('GENERAL_SETTINGS.SSO.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('GENERAL_SETTINGS.SSO.UPDATE.ERROR'));
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
|
||||
toggleSecretKey() {
|
||||
this.showSecretKey = !this.showSecretKey;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionLayout
|
||||
:title="$t('GENERAL_SETTINGS.SSO.TITLE')"
|
||||
:description="$t('GENERAL_SETTINGS.SSO.DESCRIPTION')"
|
||||
>
|
||||
<form class="grid gap-4" @submit.prevent="updateSSO">
|
||||
<WithLabel :label="$t('GENERAL_SETTINGS.SSO.FORM.ENABLE_SSO.LABEL')">
|
||||
<div class="flex items-center">
|
||||
<input v-model="ssoConfig.enabled" type="checkbox" class="mr-2" />
|
||||
<span>{{ $t('GENERAL_SETTINGS.SSO.FORM.ENABLE_SSO.HELP') }}</span>
|
||||
</div>
|
||||
</WithLabel>
|
||||
|
||||
<template v-if="ssoConfig.enabled">
|
||||
<WithLabel
|
||||
:has-error="v$.ssoConfig.provider_name.$error"
|
||||
:label="$t('GENERAL_SETTINGS.SSO.FORM.PROVIDER_NAME.LABEL')"
|
||||
:error-message="$t('GENERAL_SETTINGS.SSO.FORM.PROVIDER_NAME.ERROR')"
|
||||
>
|
||||
<NextInput
|
||||
v-model="ssoConfig.provider_name"
|
||||
type="text"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.SSO.FORM.PROVIDER_NAME.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.ssoConfig.provider_name.$touch"
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel
|
||||
:has-error="v$.ssoConfig.login_url.$error"
|
||||
:label="$t('GENERAL_SETTINGS.SSO.FORM.LOGIN_URL.LABEL')"
|
||||
:error-message="$t('GENERAL_SETTINGS.SSO.FORM.LOGIN_URL.ERROR')"
|
||||
>
|
||||
<NextInput
|
||||
v-model="ssoConfig.login_url"
|
||||
type="url"
|
||||
class="w-full"
|
||||
:placeholder="$t('GENERAL_SETTINGS.SSO.FORM.LOGIN_URL.PLACEHOLDER')"
|
||||
@blur="v$.ssoConfig.login_url.$touch"
|
||||
/>
|
||||
<template #help>
|
||||
{{ $t('GENERAL_SETTINGS.SSO.FORM.LOGIN_URL.HELP') }}
|
||||
</template>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel :label="$t('GENERAL_SETTINGS.SSO.FORM.LOGOUT_URL.LABEL')">
|
||||
<NextInput
|
||||
v-model="ssoConfig.logout_url"
|
||||
type="url"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.SSO.FORM.LOGOUT_URL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<template #help>
|
||||
{{ $t('GENERAL_SETTINGS.SSO.FORM.LOGOUT_URL.HELP') }}
|
||||
</template>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel
|
||||
:has-error="v$.ssoConfig.secret_key.$error"
|
||||
:label="$t('GENERAL_SETTINGS.SSO.FORM.SECRET_KEY.LABEL')"
|
||||
:error-message="$t('GENERAL_SETTINGS.SSO.FORM.SECRET_KEY.ERROR')"
|
||||
>
|
||||
<div class="relative">
|
||||
<NextInput
|
||||
v-model="ssoConfig.secret_key"
|
||||
:type="showSecretKey ? 'text' : 'password'"
|
||||
class="w-full pr-10"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.SSO.FORM.SECRET_KEY.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.ssoConfig.secret_key.$touch"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
@click="toggleSecretKey"
|
||||
>
|
||||
<svg
|
||||
v-if="showSecretKey"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<template #help>
|
||||
{{ $t('GENERAL_SETTINGS.SSO.FORM.SECRET_KEY.HELP') }}
|
||||
</template>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel
|
||||
:has-error="v$.ssoConfig.token_expiry.$error"
|
||||
:label="$t('GENERAL_SETTINGS.SSO.FORM.TOKEN_EXPIRY.LABEL')"
|
||||
:error-message="$t('GENERAL_SETTINGS.SSO.FORM.TOKEN_EXPIRY.ERROR')"
|
||||
>
|
||||
<NextInput
|
||||
v-model="ssoConfig.token_expiry"
|
||||
type="number"
|
||||
class="w-full"
|
||||
min="1"
|
||||
max="60"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.SSO.FORM.TOKEN_EXPIRY.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.ssoConfig.token_expiry.$touch"
|
||||
/>
|
||||
<template #help>
|
||||
{{ $t('GENERAL_SETTINGS.SSO.FORM.TOKEN_EXPIRY.HELP') }}
|
||||
</template>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<NextButton blue :is-loading="isUpdating" type="submit">
|
||||
{{ $t('GENERAL_SETTINGS.SSO.SUBMIT') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
</form>
|
||||
</SectionLayout>
|
||||
</template>
|
||||
+41
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
json.settings resource.settings
|
||||
json.sso_config resource.sso_config
|
||||
json.created_at resource.created_at
|
||||
if resource.custom_attributes.present?
|
||||
json.custom_attributes do
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddSsoConfigToAccounts < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :accounts, :sso_config, :jsonb, default: {}, null: false
|
||||
add_index :accounts, :sso_config, using: :gin
|
||||
end
|
||||
end
|
||||
+4
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2025_07_14_104358) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2025_07_18_083735) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -59,6 +59,8 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_14_104358) do
|
||||
t.integer "status", default: 0
|
||||
t.jsonb "internal_attributes", default: {}, null: false
|
||||
t.jsonb "settings", default: {}
|
||||
t.jsonb "sso_config", default: {}, null: false
|
||||
t.index ["sso_config"], name: "index_accounts_on_sso_config", using: :gin
|
||||
t.index ["status"], name: "index_accounts_on_status"
|
||||
end
|
||||
|
||||
@@ -237,7 +239,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_14_104358) do
|
||||
t.jsonb "audience", default: []
|
||||
t.datetime "scheduled_at", precision: nil
|
||||
t.boolean "trigger_only_during_business_hours", default: false
|
||||
t.jsonb "template_params"
|
||||
t.jsonb "template_params", default: {}, null: false
|
||||
t.index ["account_id"], name: "index_campaigns_on_account_id"
|
||||
t.index ["campaign_status"], name: "index_campaigns_on_campaign_status"
|
||||
t.index ["campaign_type"], name: "index_campaigns_on_campaign_type"
|
||||
|
||||
Reference in New Issue
Block a user