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.
|
||||
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
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 :check_authorization
|
||||
before_action :find_user, only: [:create]
|
||||
before_action :validate_limit, only: [:create]
|
||||
before_action :create_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
|
||||
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||
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
|
||||
|
|
|
@ -31,6 +31,10 @@ module RequestExceptionHandler
|
|||
render json: { error: message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_payment_required(message)
|
||||
render json: { error: message }, status: :payment_required
|
||||
end
|
||||
|
||||
def render_internal_server_error(message)
|
||||
render json: { error: message }, status: :internal_server_error
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# auto_resolve_duration :integer
|
||||
# domain :string(100)
|
||||
# feature_flags :integer default(0), not null
|
||||
# limits :jsonb
|
||||
# locale :integer default("en")
|
||||
# name :string not null
|
||||
# settings_flags :integer default(0), not null
|
||||
|
@ -19,6 +20,7 @@ class Account < ApplicationRecord
|
|||
include FlagShihTzu
|
||||
include Reportable
|
||||
include Featurable
|
||||
prepend_mod_with('Account')
|
||||
|
||||
DEFAULT_QUERY_SETTING = {
|
||||
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>')
|
||||
end
|
||||
|
||||
def usage_limits
|
||||
{
|
||||
agents: ChatwootApp.max_limit,
|
||||
inboxes: ChatwootApp.max_limit
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_creation
|
||||
|
|
|
@ -13,8 +13,11 @@ module Chatwoot
|
|||
# Initialize configuration defaults for originally generated Rails version.
|
||||
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('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.
|
||||
# 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 "feature_flags", default: 0, null: false
|
||||
t.integer "auto_resolve_duration"
|
||||
t.jsonb "limits", default: {}
|
||||
end
|
||||
|
||||
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'
|
||||
end
|
||||
end
|
||||
|
||||
class PlanUpgradeRequired < CustomExceptions::Base
|
||||
def message
|
||||
I18n.t 'errors.plan_upgrade_required.failed'
|
||||
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_categories).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
|
||||
|
|
Loading…
Reference in a new issue