chore: Introduce enterprise edition license (#3209)
- Initialize an "enterprise" folder that is copyrighted. - You can remove this folder and the system will continue functioning normally, in case you want a purely MIT licensed product. - Enable limit on the number of user accounts in enterprise code. - Use enterprise edition injector methods (inspired from Gitlab). - SaaS software would run enterprise edition software always. Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
2f63ebb8a6
commit
b1eea7f7d1
15 changed files with 253 additions and 3 deletions
8
LICENSE
8
LICENSE
|
@ -1,7 +1,11 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
Copyright (c) 2017-2021 Chatwoot Inc.
|
||||||
|
|
||||||
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
|
* All content that resides under the "enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "enterprise/LICENSE".
|
||||||
|
* All third party components incorporated into the Chatwoot Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
|
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
|
|
|
@ -2,6 +2,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_agent, except: [:create, :index]
|
before_action :fetch_agent, except: [:create, :index]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :find_user, only: [:create]
|
before_action :find_user, only: [:create]
|
||||||
|
before_action :validate_limit, only: [:create]
|
||||||
before_action :create_user, only: [:create]
|
before_action :create_user, only: [:create]
|
||||||
before_action :save_account_user, only: [:create]
|
before_action :save_account_user, only: [:create]
|
||||||
|
|
||||||
|
@ -69,4 +70,8 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
def agents
|
def agents
|
||||||
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_limit
|
||||||
|
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,10 @@ module RequestExceptionHandler
|
||||||
render json: { error: message }, status: :unprocessable_entity
|
render json: { error: message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_payment_required(message)
|
||||||
|
render json: { error: message }, status: :payment_required
|
||||||
|
end
|
||||||
|
|
||||||
def render_internal_server_error(message)
|
def render_internal_server_error(message)
|
||||||
render json: { error: message }, status: :internal_server_error
|
render json: { error: message }, status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
# auto_resolve_duration :integer
|
# auto_resolve_duration :integer
|
||||||
# domain :string(100)
|
# domain :string(100)
|
||||||
# feature_flags :integer default(0), not null
|
# feature_flags :integer default(0), not null
|
||||||
|
# limits :jsonb
|
||||||
# locale :integer default("en")
|
# locale :integer default("en")
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# settings_flags :integer default(0), not null
|
# settings_flags :integer default(0), not null
|
||||||
|
@ -19,6 +20,7 @@ class Account < ApplicationRecord
|
||||||
include FlagShihTzu
|
include FlagShihTzu
|
||||||
include Reportable
|
include Reportable
|
||||||
include Featurable
|
include Featurable
|
||||||
|
prepend_mod_with('Account')
|
||||||
|
|
||||||
DEFAULT_QUERY_SETTING = {
|
DEFAULT_QUERY_SETTING = {
|
||||||
flag_query_mode: :bit_operator,
|
flag_query_mode: :bit_operator,
|
||||||
|
@ -107,6 +109,13 @@ class Account < ApplicationRecord
|
||||||
super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
|
super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def usage_limits
|
||||||
|
{
|
||||||
|
agents: ChatwootApp.max_limit,
|
||||||
|
inboxes: ChatwootApp.max_limit
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def notify_creation
|
def notify_creation
|
||||||
|
|
|
@ -13,8 +13,11 @@ module Chatwoot
|
||||||
# Initialize configuration defaults for originally generated Rails version.
|
# Initialize configuration defaults for originally generated Rails version.
|
||||||
config.load_defaults 6.0
|
config.load_defaults 6.0
|
||||||
|
|
||||||
config.autoload_paths << Rails.root.join('lib')
|
|
||||||
config.eager_load_paths << Rails.root.join('lib')
|
config.eager_load_paths << Rails.root.join('lib')
|
||||||
|
config.eager_load_paths << Rails.root.join('enterprise/lib')
|
||||||
|
# rubocop:disable Rails/FilePath
|
||||||
|
config.eager_load_paths += Dir["#{Rails.root}/enterprise/app/**"]
|
||||||
|
# rubocop:enable Rails/FilePath
|
||||||
|
|
||||||
# Settings in config/environments/* take precedence over those specified here.
|
# Settings in config/environments/* take precedence over those specified here.
|
||||||
# Application configuration can go into files in config/initializers
|
# Application configuration can go into files in config/initializers
|
||||||
|
|
87
config/initializers/01_inject_enterprise_edition_module.rb
Normal file
87
config/initializers/01_inject_enterprise_edition_module.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# original Authors: Gitlab
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/initializers/0_inject_enterprise_edition_module.rb
|
||||||
|
#
|
||||||
|
|
||||||
|
### Ref: https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073
|
||||||
|
# Ancestors chain : it holds a list of constant names which are its ancestors
|
||||||
|
# example, by calling ancestors on the String class,
|
||||||
|
# String.ancestors => [String, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]
|
||||||
|
#
|
||||||
|
# Include: Ruby will insert the module into the ancestors chain of the class, just after its superclass
|
||||||
|
# ancestor chain : [OriginalClass, IncludedModule, ...]
|
||||||
|
#
|
||||||
|
# Extend: class will actually import the module methods as class methods
|
||||||
|
#
|
||||||
|
# Prepend: Ruby will look into the module methods before looking into the class.
|
||||||
|
# ancestor chain : [PrependedModule, OriginalClass, ...]
|
||||||
|
########
|
||||||
|
|
||||||
|
require 'active_support/inflector'
|
||||||
|
|
||||||
|
module InjectEnterpriseEditionModule
|
||||||
|
def prepend_mod_with(constant_name, namespace: Object, with_descendants: false)
|
||||||
|
each_extension_for(constant_name, namespace) do |constant|
|
||||||
|
prepend_module(constant, with_descendants)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extend_mod_with(constant_name, namespace: Object)
|
||||||
|
# rubocop:disable Performance/MethodObjectAsBlock
|
||||||
|
each_extension_for(
|
||||||
|
constant_name,
|
||||||
|
namespace,
|
||||||
|
&method(:extend)
|
||||||
|
)
|
||||||
|
# rubocop:enable Performance/MethodObjectAsBlock
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_mod_with(constant_name, namespace: Object)
|
||||||
|
# rubocop:disable Performance/MethodObjectAsBlock
|
||||||
|
each_extension_for(
|
||||||
|
constant_name,
|
||||||
|
namespace,
|
||||||
|
&method(:include)
|
||||||
|
)
|
||||||
|
# rubocop:enable Performance/MethodObjectAsBlock
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepend_mod(with_descendants: false)
|
||||||
|
prepend_mod_with(name, with_descendants: with_descendants)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extend_mod
|
||||||
|
extend_mod_with(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_mod
|
||||||
|
include_mod_with(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prepend_module(mod, with_descendants)
|
||||||
|
prepend(mod)
|
||||||
|
|
||||||
|
descendants.each { |descendant| descendant.prepend(mod) } if with_descendants
|
||||||
|
end
|
||||||
|
|
||||||
|
def each_extension_for(constant_name, namespace)
|
||||||
|
ChatwootApp.extensions.each do |extension_name|
|
||||||
|
extension_namespace =
|
||||||
|
const_get_maybe_false(namespace, extension_name.camelize)
|
||||||
|
|
||||||
|
extension_module =
|
||||||
|
const_get_maybe_false(extension_namespace, constant_name)
|
||||||
|
|
||||||
|
yield(extension_module) if extension_module
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def const_get_maybe_false(mod, name)
|
||||||
|
mod&.const_defined?(name, false) && mod&.const_get(name, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Module.prepend(InjectEnterpriseEditionModule)
|
5
db/migrate/20211012135050_add_limits_to_accounts.rb
Normal file
5
db/migrate/20211012135050_add_limits_to_accounts.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AddLimitsToAccounts < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :accounts, :limits, :jsonb, default: {}
|
||||||
|
end
|
||||||
|
end
|
|
@ -52,6 +52,7 @@ ActiveRecord::Schema.define(version: 2021_12_08_085931) do
|
||||||
t.integer "settings_flags", default: 0, null: false
|
t.integer "settings_flags", default: 0, null: false
|
||||||
t.integer "feature_flags", default: 0, null: false
|
t.integer "feature_flags", default: 0, null: false
|
||||||
t.integer "auto_resolve_duration"
|
t.integer "auto_resolve_duration"
|
||||||
|
t.jsonb "limits", default: {}
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "action_mailbox_inbound_emails", force: :cascade do |t|
|
create_table "action_mailbox_inbound_emails", force: :cascade do |t|
|
||||||
|
|
36
enterprise/LICENSE
Normal file
36
enterprise/LICENSE
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
The Chatwoot Enterprise license (the “Enterprise License”)
|
||||||
|
Copyright (c) 2017-2021 Chatwoot Inc
|
||||||
|
|
||||||
|
With regard to the Chatwoot Software:
|
||||||
|
|
||||||
|
This software and associated documentation files (the "Software") may only be
|
||||||
|
used in production, if you (and any entity that you represent) have agreed to,
|
||||||
|
and are in compliance with, the Chatwoot Subscription Terms of Service, available
|
||||||
|
at https://www.chatwoot.com/terms-of-service/ (the “Enterprise Terms”), or other
|
||||||
|
agreement governing the use of the Software, as agreed by you and Chatwoot,
|
||||||
|
and otherwise have a valid Chatwoot Enterprise License for for the
|
||||||
|
correct number of user seats. Subject to the foregoing sentence, you are free to
|
||||||
|
modify this Software and publish patches to the Software. You agree that Chatwoot
|
||||||
|
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||||
|
to all such modifications and/or patches, and all such modifications and/or
|
||||||
|
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||||
|
exploited with a valid Chatwoot Enterprise subscription for the correct
|
||||||
|
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
||||||
|
the Software for development and testing purposes, without requiring a
|
||||||
|
subscription. You agree that Chatwoot and/or its licensors (as applicable) retain
|
||||||
|
all right, title and interest in and to all such modifications. You are not
|
||||||
|
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||||
|
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
For all third party components incorporated into the Chatwoot Software, those
|
||||||
|
components are licensed under the original license provided by the owner of the
|
||||||
|
applicable component.
|
15
enterprise/app/models/enterprise/account.rb
Normal file
15
enterprise/app/models/enterprise/account.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Enterprise::Account
|
||||||
|
def usage_limits
|
||||||
|
{
|
||||||
|
agents: get_limits(:agents),
|
||||||
|
inboxes: get_limits(:inboxes)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_limits(limit_name)
|
||||||
|
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
|
||||||
|
self[:limits][limit_name.to_s] || GlobalConfig.get(config_name)[config_name] || ChatwootApp.max_limit
|
||||||
|
end
|
||||||
|
end
|
2
enterprise/lib/enterprise.rb
Normal file
2
enterprise/lib/enterprise.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module Enterprise
|
||||||
|
end
|
33
lib/chatwoot_app.rb
Normal file
33
lib/chatwoot_app.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
|
module ChatwootApp
|
||||||
|
def self.root
|
||||||
|
Pathname.new(File.expand_path('..', __dir__))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.max_limit
|
||||||
|
100_000
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.enterprise?
|
||||||
|
return if ENV.fetch('DISABLE_ENTERPRISE', false)
|
||||||
|
|
||||||
|
@enterprise ||= root.join('enterprise').exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.custom?
|
||||||
|
@custom ||= root.join('custom').exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.extensions
|
||||||
|
if custom?
|
||||||
|
%w[enterprise custom]
|
||||||
|
elsif enterprise?
|
||||||
|
%w[enterprise]
|
||||||
|
else
|
||||||
|
%w[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,4 +28,10 @@ module CustomExceptions::Account
|
||||||
I18n.t 'errors.signup.failed'
|
I18n.t 'errors.signup.failed'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class PlanUpgradeRequired < CustomExceptions::Base
|
||||||
|
def message
|
||||||
|
I18n.t 'errors.plan_upgrade_required.failed'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
32
spec/enterprise/models/account_spec.rb
Normal file
32
spec/enterprise/models/account_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Account do
|
||||||
|
describe 'usage_limits' do
|
||||||
|
before do
|
||||||
|
create(:installation_config, name: 'ACCOUNT_AGENTS_LIMIT', value: 20)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
|
||||||
|
it 'returns max limits from global config when enterprise version' do
|
||||||
|
expect(account.usage_limits).to eq(
|
||||||
|
{
|
||||||
|
agents: 20,
|
||||||
|
inboxes: ChatwootApp.max_limit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns max limits from account when enterprise version' do
|
||||||
|
account.update(limits: { agents: 10 })
|
||||||
|
expect(account.usage_limits).to eq(
|
||||||
|
{
|
||||||
|
agents: 10,
|
||||||
|
inboxes: ChatwootApp.max_limit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,4 +21,12 @@ RSpec.describe Account do
|
||||||
it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) }
|
it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) }
|
||||||
it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) }
|
it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) }
|
||||||
it { is_expected.to have_many(:teams).dependent(:destroy_async) }
|
it { is_expected.to have_many(:teams).dependent(:destroy_async) }
|
||||||
|
|
||||||
|
describe 'usage_limits' do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
|
||||||
|
it 'returns ChatwootApp.max limits' do
|
||||||
|
expect(account.usage_limits).to eq({ agents: ChatwootApp.max_limit, inboxes: ChatwootApp.max_limit })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue