app/models/tenant.rb
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
scope :only_hidden, -> { where.not(hidden_at: nil) }
def self.find_by_domain(host)
domain.find_by(schema: host)
end
def self.resolve_host(host)
return nil if Rails.application.config.multitenancy.blank?
return nil if host.blank? || host.match?(Resolv::AddressRegex)
schema = schema_for(host)
if schema && only_hidden.find_by(schema: schema)
raise Apartment::TenantNotFound
else
schema
end
end
def self.schema_for(host)
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
if find_by_domain(schema)
raise Apartment::TenantNotFound
else
schema
end
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 = Rails.application.secrets
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
subfolder_path_for(current_schema)
end
def self.subfolder_path_for(schema)
if schema == "public"
""
else
File.join("tenants", 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(&)
["public"].union(Apartment.tenant_names).each do |schema|
switch(schema, &)
end
end
def host
self.class.host_for(schema)
end
def hide
update_attribute(:hidden_at, Time.current)
end
def restore
update_attribute(:hidden_at, nil)
end
def hidden?
hidden_at.present?
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}\";"
)
rename_storage
end
end
def rename_storage
service = ActiveStorage::Blob.service
return unless service.respond_to?(:tenant_root_for)
old_storage = service.tenant_root_for(schema_before_last_save)
return unless File.directory?(old_storage)
new_storage = service.tenant_root_for(schema)
File.rename(old_storage, new_storage)
end
def destroy_schema
Apartment::Tenant.drop(schema)
end
end