diff --git a/.gitignore b/.gitignore
index 0c71b2055..e4b14d2f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,6 @@ package-lock.json
# cypress
test/cypress/videos/*
+
+/config/master.key
+/config/*.enc
\ No newline at end of file
diff --git a/app/controllers/super_admin/installation_configs_controller.rb b/app/controllers/super_admin/installation_configs_controller.rb
new file mode 100644
index 000000000..36a45f707
--- /dev/null
+++ b/app/controllers/super_admin/installation_configs_controller.rb
@@ -0,0 +1,46 @@
+class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
+ # Overwrite any of the RESTful controller actions to implement custom behavior
+ # For example, you may want to send an email after a foo is updated.
+ #
+ # def update
+ # super
+ # send_foo_updated_email(requested_resource)
+ # end
+
+ # Override this method to specify custom lookup behavior.
+ # This will be used to set the resource for the `show`, `edit`, and `update`
+ # actions.
+ #
+ # def find_resource(param)
+ # Foo.find_by!(slug: param)
+ # end
+
+ # The result of this lookup will be available as `requested_resource`
+
+ # Override this if you have certain roles that require a subset
+ # this will be used to set the records shown on the `index` action.
+ #
+ def scoped_resource
+ resource_class.editable
+ end
+
+ # Override `resource_params` if you want to transform the submitted
+ # data before it's persisted. For example, the following would turn all
+ # empty values into nil values. It uses other APIs such as `resource_class`
+ # and `dashboard`:
+ #
+ # def resource_params
+ # params.require(resource_class.model_name.param_key).
+ # permit(dashboard.permitted_attributes).
+ # transform_values { |value| value == "" ? nil : value }
+ # end
+
+ def resource_params
+ params.require(:installation_config)
+ .permit(:name, :value)
+ .transform_values { |value| value == '' ? nil : value }.merge(locked: false)
+ end
+
+ # See https://administrate-prototype.herokuapp.com/customizing_controller_actions
+ # for more information
+end
diff --git a/app/dashboards/installation_config_dashboard.rb b/app/dashboards/installation_config_dashboard.rb
new file mode 100644
index 000000000..8a60c0dba
--- /dev/null
+++ b/app/dashboards/installation_config_dashboard.rb
@@ -0,0 +1,66 @@
+require 'administrate/base_dashboard'
+
+class InstallationConfigDashboard < Administrate::BaseDashboard
+ # ATTRIBUTE_TYPES
+ # a hash that describes the type of each of the model's fields.
+ #
+ # Each different type represents an Administrate::Field object,
+ # which determines how the attribute is displayed
+ # on pages throughout the dashboard.
+ ATTRIBUTE_TYPES = {
+ id: Field::Number,
+ name: Field::String,
+ value: SerializedField,
+ created_at: Field::DateTime,
+ updated_at: Field::DateTime
+ }.freeze
+
+ # COLLECTION_ATTRIBUTES
+ # an array of attributes that will be displayed on the model's index page.
+ #
+ # By default, it's limited to four items to reduce clutter on index pages.
+ # Feel free to add, remove, or rearrange items.
+ COLLECTION_ATTRIBUTES = %i[
+ id
+ name
+ value
+ created_at
+ ].freeze
+
+ # SHOW_PAGE_ATTRIBUTES
+ # an array of attributes that will be displayed on the model's show page.
+ SHOW_PAGE_ATTRIBUTES = %i[
+ id
+ name
+ value
+ created_at
+ updated_at
+ ].freeze
+
+ # FORM_ATTRIBUTES
+ # an array of attributes that will be displayed
+ # on the model's form (`new` and `edit`) pages.
+ FORM_ATTRIBUTES = %i[
+ name
+ value
+ ].freeze
+
+ # COLLECTION_FILTERS
+ # a hash that defines filters that can be used while searching via the search
+ # field of the dashboard.
+ #
+ # For example to add an option to search for open resources by typing "open:"
+ # in the search field:
+ #
+ # COLLECTION_FILTERS = {
+ # open: ->(resources) { resources.where(open: true) }
+ # }.freeze
+ COLLECTION_FILTERS = {}.freeze
+
+ # Overwrite this method to customize how installation configs are displayed
+ # across all pages of the admin dashboard.
+ #
+ # def display_resource(installation_config)
+ # "InstallationConfig ##{installation_config.id}"
+ # end
+end
diff --git a/app/fields/serialized_field.rb b/app/fields/serialized_field.rb
new file mode 100644
index 000000000..5c1069c42
--- /dev/null
+++ b/app/fields/serialized_field.rb
@@ -0,0 +1,15 @@
+require 'administrate/field/base'
+
+class SerializedField < Administrate::Field::Base
+ def to_s
+ hash? ? data.as_json : data.to_s
+ end
+
+ def hash?
+ data.is_a? Hash
+ end
+
+ def array?
+ data.is_a? Array
+ end
+end
diff --git a/app/models/installation_config.rb b/app/models/installation_config.rb
index 589ab3a36..ce65eb678 100644
--- a/app/models/installation_config.rb
+++ b/app/models/installation_config.rb
@@ -3,6 +3,7 @@
# Table name: installation_configs
#
# id :bigint not null, primary key
+# locked :boolean default(TRUE), not null
# name :string not null
# serialized_value :jsonb not null
# created_at :datetime not null
@@ -10,14 +11,17 @@
#
# Indexes
#
+# index_installation_configs_on_name (name) UNIQUE
# index_installation_configs_on_name_and_created_at (name,created_at) UNIQUE
#
class InstallationConfig < ApplicationRecord
serialize :serialized_value, HashWithIndifferentAccess
+ before_validation :set_lock
validates :name, presence: true
default_scope { order(created_at: :desc) }
+ scope :editable, -> { where(locked: false) }
def value
serialized_value[:value]
@@ -28,4 +32,10 @@ class InstallationConfig < ApplicationRecord
value: value_to_assigned
}.with_indifferent_access
end
+
+ private
+
+ def set_lock
+ self.locked = true if locked.nil?
+ end
end
diff --git a/app/views/fields/serialized_field/_form.html.erb b/app/views/fields/serialized_field/_form.html.erb
new file mode 100644
index 000000000..946eb0187
--- /dev/null
+++ b/app/views/fields/serialized_field/_form.html.erb
@@ -0,0 +1,19 @@
+
+ <%= f.label field.attribute, class: "field-unit__label" %>
+
+ <% if field.array? %>
+ <% field.data.each do |sub_field| %>
+ <%= f.fields_for "#{field.attribute}[]", field.resource do |values_form| %>
+
+ <% sub_field.each do |sf_key, sf_value| %>
+ <%= values_form.label sf_key %>
+ <%= values_form.text_field sf_key, value: sf_value, disabled: true %>
+ <% end %>
+
+ <% end %>
+ <% end %>
+ <% else %>
+ <%= f.text_field field.attribute %>
+ <% end %>
+
+
diff --git a/app/views/fields/serialized_field/_index.html.erb b/app/views/fields/serialized_field/_index.html.erb
new file mode 100644
index 000000000..a3e1ce420
--- /dev/null
+++ b/app/views/fields/serialized_field/_index.html.erb
@@ -0,0 +1,9 @@
+<% if field.array? %>
+ <% field.data.each do |sub_field| %>
+
+ <%= sub_field.to_s %>
+
+ <% end %>
+<% else %>
+ <%= field.to_s %>
+<% end %>
diff --git a/app/views/fields/serialized_field/_show.html.erb b/app/views/fields/serialized_field/_show.html.erb
new file mode 100644
index 000000000..a3e1ce420
--- /dev/null
+++ b/app/views/fields/serialized_field/_show.html.erb
@@ -0,0 +1,9 @@
+<% if field.array? %>
+ <% field.data.each do |sub_field| %>
+
+ <%= sub_field.to_s %>
+
+ <% end %>
+<% else %>
+ <%= field.to_s %>
+<% end %>
diff --git a/app/views/super_admin/application/_navigation.html.erb b/app/views/super_admin/application/_navigation.html.erb
index 54c2ecc1b..09810f053 100644
--- a/app/views/super_admin/application/_navigation.html.erb
+++ b/app/views/super_admin/application/_navigation.html.erb
@@ -15,7 +15,8 @@ as defined by the routes in the `admin/` namespace
accounts: 'ion ion-briefcase',
users: 'ion ion-person-stalker',
super_admins: 'ion ion-unlocked',
- access_tokens: 'ion-key'
+ access_tokens: 'ion-key',
+ installation_configs: 'ion ion-settings'
}
%>
@@ -43,7 +44,7 @@ as defined by the routes in the `admin/` namespace
- <%= link_to "Sidekiq", sidekiq_web_url %>
+ <%= link_to "Sidekiq", sidekiq_web_url, { target: "_blank" } %>
diff --git a/config/installation_config.yml b/config/installation_config.yml
index 41c27b6c5..694332921 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -1,26 +1,37 @@
-- name: LOGO_THUMBNAIL
- value: '/brand-assets/logo_thumbnail.svg'
-- name: LOGO
- value: '/brand-assets/logo.svg'
+# if you dont specify locked attribute, the default value will be true
+# which means the particular config will be locked
- name: INSTALLATION_NAME
value: 'Chatwoot'
+ locked: false
+- name: LOGO_THUMBNAIL
+ value: '/brand-assets/logo_thumbnail.svg'
+ locked: true
+- name: LOGO
+ value: '/brand-assets/logo.svg'
- name: BRAND_URL
value: 'https://www.chatwoot.com'
- name: WIDGET_BRAND_URL
value: 'https://www.chatwoot.com'
-- name: TERMS_URL
- value: 'https://www.chatwoot.com/terms-of-service'
-- name: PRIVACY_URL
- value: 'https://www.chatwoot.com/privacy-policy'
-- name: DISPLAY_MANIFEST
- value: true
-- name: MAILER_INBOUND_EMAIL_DOMAIN
- value:
-- name: MAILER_SUPPORT_EMAIL
- value:
-- name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD
- value: false
- name: BRAND_NAME
value: 'Chatwoot'
+- name: TERMS_URL
+ value: 'https://www.chatwoot.com/terms-of-service'
+ locked: false
+- name: PRIVACY_URL
+ value: 'https://www.chatwoot.com/privacy-policy'
+ locked: false
+- name: DISPLAY_MANIFEST
+ value: true
+ locked: false
+- name: MAILER_INBOUND_EMAIL_DOMAIN
+ value:
+ locked: false
+- name: MAILER_SUPPORT_EMAIL
+ value:
+ locked: false
+- name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD
+ value: false
+ locked: false
- name: 'INSTALLATION_EVENTS_WEBHOOK_URL'
value:
+ locked: false
diff --git a/config/routes.rb b/config/routes.rb
index aa888627a..7d1715dea 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -219,6 +219,7 @@ Rails.application.routes.draw do
resources :users, only: [:index, :new, :create, :show, :edit, :update]
resources :super_admins
resources :access_tokens, only: [:index, :show]
+ resources :installation_configs, only: [:index, :new, :create, :show, :edit, :update]
# resources that doesn't appear in primary navigation in super admin
resources :account_users, only: [:new, :create, :destroy]
diff --git a/db/migrate/20210113045116_add_locked_attribute_to_installation_config.rb b/db/migrate/20210113045116_add_locked_attribute_to_installation_config.rb
new file mode 100644
index 000000000..c3715d244
--- /dev/null
+++ b/db/migrate/20210113045116_add_locked_attribute_to_installation_config.rb
@@ -0,0 +1,26 @@
+class AddLockedAttributeToInstallationConfig < ActiveRecord::Migration[6.0]
+ def up
+ add_column :installation_configs, :locked, :boolean, default: true, null: false
+ purge_duplicates
+ add_index :installation_configs, :name, unique: true
+ end
+
+ def down
+ remove_column :installation_configs, :locked
+ remove_index :installation_configs, :name
+ end
+
+ def purge_duplicates
+ config_names = InstallationConfig.all.map(&:name).uniq
+
+ config_names.each do |name|
+ ids = InstallationConfig.where(name: name).pluck(&:id)
+ next if ids.size <= 1
+
+ # preserve the last config and destroy rest
+ ids.sort!
+ ids.pop
+ InstallationConfig.where(id: ids).destroy_all
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index eafa19f6e..0cb2734a8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,8 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-
-ActiveRecord::Schema.define(version: 2021_01_09_211805) do
+ActiveRecord::Schema.define(version: 2021_01_13_045116) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -293,7 +292,9 @@ ActiveRecord::Schema.define(version: 2021_01_09_211805) do
t.jsonb "serialized_value", default: {}, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.boolean "locked", default: true, null: false
t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true
+ t.index ["name"], name: "index_installation_configs_on_name", unique: true
end
create_table "integrations_hooks", force: :cascade do |t|
diff --git a/lib/config_loader.rb b/lib/config_loader.rb
index e880d3dd1..7c7933934 100644
--- a/lib/config_loader.rb
+++ b/lib/config_loader.rb
@@ -44,19 +44,25 @@ class ConfigLoader
end
end
- def save_general_config(existing_config, new_config)
- if existing_config
+ def save_general_config(existing, latest)
+ if existing
# save config only if reconcile flag is false and existing configs value does not match default value
- save_as_new_config(new_config) if !@reconcile_only_new && existing_config.value != new_config[:value]
+ save_as_new_config(latest) if !@reconcile_only_new && compare_values(existing, latest)
else
- save_as_new_config(new_config)
+ save_as_new_config(latest)
end
end
- def save_as_new_config(new_config)
- config = InstallationConfig.new(name: new_config[:name])
- config.value = new_config[:value]
- config.save
+ def compare_values(existing, latest)
+ existing.value != latest[:value] ||
+ (!latest[:locked].nil? && existing.locked != latest[:locked])
+ end
+
+ def save_as_new_config(latest)
+ config = InstallationConfig.find_or_create_by(name: latest[:name])
+ config.value = latest[:value]
+ config.locked = latest[:locked]
+ config.save!
end
def reconcile_feature_config
@@ -67,7 +73,7 @@ class ConfigLoader
compare_and_save_feature(config)
else
- save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: account_features })
+ save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: account_features, locked: true })
end
end
@@ -79,6 +85,6 @@ class ConfigLoader
# update the existing feature flag values with default values and add new feature flags with default values
(account_features + config.value).uniq { |h| h['name'] }
end
- config.update({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features })
+ config.update({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features, locked: true })
end
end
diff --git a/spec/controllers/super_admin/installation_configs_controller_spec.rb b/spec/controllers/super_admin/installation_configs_controller_spec.rb
new file mode 100644
index 000000000..263e63c24
--- /dev/null
+++ b/spec/controllers/super_admin/installation_configs_controller_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+RSpec.describe 'Super Admin Installation Config API', type: :request do
+ let(:super_admin) { create(:super_admin) }
+
+ describe 'GET /super_admin/installation_configs/new' do
+ context 'when it is an unauthenticated super admin' do
+ it 'returns unauthorized' do
+ get '/super_admin/installation_configs/new'
+ expect(response).to have_http_status(:redirect)
+ end
+ end
+
+ context 'when it is an authenticated super admin' do
+ let(:config) { create(:installation_config, { name: 'TESTCONFIG', value: 'TESTVALUE', locked: false }) }
+
+ before do
+ config
+ end
+
+ it 'shows the installation_configs create page' do
+ sign_in super_admin
+ get '/super_admin/installation_configs/new'
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'shows the installation_configs edit page' do
+ sign_in super_admin
+ editable_config = InstallationConfig.editable.first
+ get "/super_admin/installation_configs/#{editable_config.id}/edit"
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'shows the installation_configs list page' do
+ sign_in super_admin
+ get '/super_admin/installation_configs'
+ expect(response).to have_http_status(:success)
+ expect(response.body).to include(config.name)
+ end
+ end
+ end
+end
diff --git a/spec/factories/installation_config.rb b/spec/factories/installation_config.rb
index d78aead4d..01c89db85 100644
--- a/spec/factories/installation_config.rb
+++ b/spec/factories/installation_config.rb
@@ -2,5 +2,6 @@ FactoryBot.define do
factory :installation_config do
name { 'xyc' }
value { 1.5 }
+ locked { true }
end
end