Some institutions using CONSUL have expressed interest in this feature since some of their tenants might already have their own domains. We've considered many options for the user interface to select whether we're using a subdomain or a domain, like having two separate fields, using a check box, ... In the end we've chosen radio buttons because they make it easier to follow a logical sequence: first you decide whether you're introducing a domain or subdomain, and then you enter it. We've also considered hiding this option and assuming "if it's got a dot, it's a domain". However, this wouldn't work with nested subdomains and it wouldn't work with domains which are simply machine names. Note that a group of radio buttons (or check boxes) is difficult to style when the text of the label might expand over more than one line (as is the case here on small screens); in this case, most solutions result in the second line of the label appearing immediately under the radio button, instead of being aligned with the first line of the label. That's why I've added a container for the input+label combination.
154 lines
3.2 KiB
Ruby
154 lines
3.2 KiB
Ruby
class Tenant < ApplicationRecord
|
|
enum schema_type: %w[subdomain domain]
|
|
|
|
validates :schema,
|
|
presence: true,
|
|
uniqueness: true,
|
|
exclusion: { in: ->(*) { excluded_subdomains }},
|
|
format: { with: URI::DEFAULT_PARSER.regexp[:HOST] }
|
|
validates :name, presence: true, uniqueness: true
|
|
|
|
after_create :create_schema
|
|
after_update :rename_schema
|
|
after_destroy :destroy_schema
|
|
|
|
def self.find_by_domain(host)
|
|
domain.find_by(schema: host)
|
|
end
|
|
|
|
def self.resolve_host(host)
|
|
return nil unless Rails.application.config.multitenancy.present?
|
|
return nil if host.blank? || host.match?(Resolv::AddressRegex)
|
|
|
|
host_without_www = host.delete_prefix("www.")
|
|
|
|
if find_by_domain(host)
|
|
host
|
|
elsif find_by_domain(host_without_www)
|
|
host_without_www
|
|
else
|
|
host_domain = allowed_domains.find { |domain| host == domain || host.ends_with?(".#{domain}") }
|
|
schema = host_without_www.sub(/\.?#{host_domain}\Z/, "").presence
|
|
|
|
schema unless find_by_domain(schema)
|
|
end
|
|
end
|
|
|
|
def self.allowed_domains
|
|
dev_domains = %w[localhost lvh.me example.com]
|
|
dev_domains + [default_host]
|
|
end
|
|
|
|
def self.excluded_subdomains
|
|
%w[mail public shared_extensions www]
|
|
end
|
|
|
|
def self.default_url_options
|
|
ActionMailer::Base.default_url_options
|
|
end
|
|
|
|
def self.default_host
|
|
default_url_options[:host]
|
|
end
|
|
|
|
def self.default_domain
|
|
if default_host == "localhost"
|
|
"lvh.me"
|
|
else
|
|
default_host
|
|
end
|
|
end
|
|
|
|
def self.current_url_options
|
|
default_url_options.merge(host: current_host)
|
|
end
|
|
|
|
def self.current_host
|
|
host_for(current_schema)
|
|
end
|
|
|
|
def self.host_for(schema)
|
|
if schema == "public"
|
|
default_host
|
|
elsif find_by_domain(schema)
|
|
schema
|
|
else
|
|
"#{schema}.#{default_domain}"
|
|
end
|
|
end
|
|
|
|
def self.current_secrets
|
|
if default?
|
|
Rails.application.secrets
|
|
else
|
|
@secrets ||= {}
|
|
@cached_rails_secrets ||= Rails.application.secrets
|
|
|
|
if @cached_rails_secrets != Rails.application.secrets
|
|
@secrets = {}
|
|
@cached_rails_secrets = nil
|
|
end
|
|
|
|
@secrets[current_schema] ||= Rails.application.secrets.merge(
|
|
Rails.application.secrets.dig(:tenants, current_schema.to_sym).to_h
|
|
)
|
|
end
|
|
end
|
|
|
|
def self.subfolder_path
|
|
if default?
|
|
""
|
|
else
|
|
File.join("tenants", current_schema)
|
|
end
|
|
end
|
|
|
|
def self.path_with_subfolder(filename_or_folder)
|
|
File.join(subfolder_path, filename_or_folder).delete_prefix("/")
|
|
end
|
|
|
|
def self.default?
|
|
current_schema == "public"
|
|
end
|
|
|
|
def self.current_schema
|
|
Apartment::Tenant.current
|
|
end
|
|
|
|
def self.current
|
|
find_by(schema: current_schema)
|
|
end
|
|
|
|
def self.switch(...)
|
|
Apartment::Tenant.switch(...)
|
|
end
|
|
|
|
def self.run_on_each(&block)
|
|
["public"].union(Apartment.tenant_names).each do |schema|
|
|
switch(schema, &block)
|
|
end
|
|
end
|
|
|
|
def host
|
|
self.class.host_for(schema)
|
|
end
|
|
|
|
private
|
|
|
|
def create_schema
|
|
Apartment::Tenant.create(schema)
|
|
end
|
|
|
|
def rename_schema
|
|
if saved_change_to_schema?
|
|
ActiveRecord::Base.connection.execute(
|
|
"ALTER SCHEMA \"#{schema_before_last_save}\" RENAME TO \"#{schema}\";"
|
|
)
|
|
end
|
|
end
|
|
|
|
def destroy_schema
|
|
Apartment::Tenant.drop(schema)
|
|
end
|
|
end
|