We moved this file to `app/lib/` in commit cb477149c so it would be in
the autoload_paths. However, this class is loaded by ActiveStorage, with
the following method:
```
def resolve(class_name)
require "active_storage/service/#{class_name.to_s.underscore}_service"
ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
rescue LoadError
raise "Missing service adapter for #{class_name.inspect}"
end
``
So this file needs to be in the $LOAD_PATH, or else ActiveStorage won't
be able to load it when we disable the `add_autoload_paths_to_load_path`
option, which is the default in Rails 7.1 [1].
Moving it to the `lib` folder solves the issue; as mentioned in the
guide to upgrade to Rails 7.1 [2]:
> The lib directory is not affected by this flag, it is added to
> $LOAD_PATH always.
However, we were also referencing this class in the `Tenant` model,
meaning we needed to autoload it as well somehow. So, instead of
directly referencing this class, we're using `respond_to?` in the Tenant
model.
We're changing the test so it fails when the code calls
`is_a?(ActiveStorage::Service::TenantDiskService)`. We need to change
the active storage configurations in the test because, otherwise, the
moment `ActiveStorage::Blob` is loaded, the `TenantDiskService` class is
also loaded, meaning the test will pass when using `is_a?`.
Note that, since this class isn't in the autoload paths anymore, we need
to add a `require` in the tests. We could add an initializer to require
it; we're not doing it in order to be consistent with what ActiveStorage
does: it only loads the service that's going to be used in the current
Rails environment. If somebody changed their production environment in
order to use (for example), S3, and we added an initializer to require
the TenantDiskService, we would still load the TenantDiskService even if
it isn't going to be used.
[1] https://guides.rubyonrails.org/v7.1/configuring.html#config-add-autoload-paths-to-load-path
[2] https://guides.rubyonrails.org/v7.1/upgrading_ruby_on_rails.html#autoloaded-paths-are-no-longer-in-$load-path
201 lines
4.2 KiB
Ruby
201 lines
4.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
|
|
|
|
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
|