class Tenant < ApplicationRecord enum :schema_type, { subdomain: 0, domain: 1 } 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