diff --git a/.rubocop.yml b/.rubocop.yml index 9e252d614..f47dd6fb6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -335,6 +335,7 @@ Rails/SkipsModelValidations: ForbiddenMethods: - update_attribute Exclude: + - app/models/tenant.rb - lib/acts_as_paranoid_aliases.rb Rails/TimeZone: diff --git a/app/components/admin/tenants/index_component.html.erb b/app/components/admin/tenants/index_component.html.erb index ad5f43ea9..63bb3504e 100644 --- a/app/components/admin/tenants/index_component.html.erb +++ b/app/components/admin/tenants/index_component.html.erb @@ -8,20 +8,14 @@ <%= attribute_name(:name) %> <%= attribute_name(:schema) %> <%= attribute_name(:url) %> + <%= t("admin.tenants.index.enabled") %> <%= t("admin.shared.actions") %> <% @tenants.each do |tenant| %> - - <%= tenant.name %> - <%= tenant.schema %> - <%= link_to tenant.host, root_url(host: tenant.host) %> - - <%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) %> - - + <%= render Admin::Tenants::RowComponent.new(tenant) %> <% end %> diff --git a/app/components/admin/tenants/row_component.html.erb b/app/components/admin/tenants/row_component.html.erb new file mode 100644 index 000000000..365866676 --- /dev/null +++ b/app/components/admin/tenants/row_component.html.erb @@ -0,0 +1,9 @@ + + <%= tenant.name %> + <%= tenant.schema %> + <%= link_to_unless tenant.hidden?, tenant.host, root_url(host: tenant.host) %> + <%= render Admin::Tenants::ToggleHiddenComponent.new(tenant) %> + + <%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) %> + + diff --git a/app/components/admin/tenants/row_component.rb b/app/components/admin/tenants/row_component.rb new file mode 100644 index 000000000..648b0db7a --- /dev/null +++ b/app/components/admin/tenants/row_component.rb @@ -0,0 +1,7 @@ +class Admin::Tenants::RowComponent < ApplicationComponent + attr_reader :tenant + + def initialize(tenant) + @tenant = tenant + end +end diff --git a/app/components/admin/tenants/toggle_hidden_component.html.erb b/app/components/admin/tenants/toggle_hidden_component.html.erb new file mode 100644 index 000000000..64a3a2e93 --- /dev/null +++ b/app/components/admin/tenants/toggle_hidden_component.html.erb @@ -0,0 +1 @@ +<%= render Admin::ToggleSwitchComponent.new(action, tenant, pressed: enabled?, **options) %> diff --git a/app/components/admin/tenants/toggle_hidden_component.rb b/app/components/admin/tenants/toggle_hidden_component.rb new file mode 100644 index 000000000..12488ce21 --- /dev/null +++ b/app/components/admin/tenants/toggle_hidden_component.rb @@ -0,0 +1,28 @@ +class Admin::Tenants::ToggleHiddenComponent < ApplicationComponent + attr_reader :tenant + + def initialize(tenant) + @tenant = tenant + end + + private + + def action + if enabled? + :hide + else + :restore + end + end + + def options + { + method: :put, + "aria-label": t("admin.tenants.index.enable", tenant: tenant.name) + } + end + + def enabled? + !tenant.hidden? + end +end diff --git a/app/controllers/admin/tenants_controller.rb b/app/controllers/admin/tenants_controller.rb index a43e88166..e7d6e949a 100644 --- a/app/controllers/admin/tenants_controller.rb +++ b/app/controllers/admin/tenants_controller.rb @@ -27,6 +27,24 @@ class Admin::TenantsController < Admin::BaseController end end + def hide + @tenant.hide + + respond_to do |format| + format.html { redirect_to admin_tenants_path, notice: t("admin.tenants.hide.notice") } + format.js { render template: "admin/tenants/toggle_enabled" } + end + end + + def restore + @tenant.restore + + respond_to do |format| + format.html { redirect_to admin_tenants_path, notice: t("admin.tenants.restore.notice") } + format.js { render template: "admin/tenants/toggle_enabled" } + end + end + private def tenant_params diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 9e3556e80..f874af58b 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -136,7 +136,7 @@ module Abilities can [:create, :read], LocalCensusRecords::Import if Rails.application.config.multitenancy && Tenant.default? - can [:create, :read, :update], Tenant + can [:create, :read, :update, :hide, :restore], Tenant end end end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index ffbba3442..654cbecf0 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -12,6 +12,8 @@ class Tenant < ApplicationRecord 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 @@ -20,6 +22,16 @@ class Tenant < ApplicationRecord return nil unless Rails.application.config.multitenancy.present? 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) @@ -137,6 +149,18 @@ class Tenant < ApplicationRecord 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 diff --git a/app/views/admin/tenants/toggle_enabled.js.erb b/app/views/admin/tenants/toggle_enabled.js.erb new file mode 100644 index 000000000..8e2a0eadc --- /dev/null +++ b/app/views/admin/tenants/toggle_enabled.js.erb @@ -0,0 +1,4 @@ +var replacement = $("<%= j render Admin::Tenants::RowComponent.new(@tenant) %>"); +var row = $("#<%= dom_id(@tenant) %>"); + +row.html(replacement.html()).find(".toggle-switch [type='submit']").focus(); diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index 4f59150ad..b128ec8cf 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -1635,10 +1635,16 @@ en: form: use_subdomain: "Use a subdomain in the %{domain} domain to access this tenant" use_domain: "Use a different domain to access this tenant" + hide: + notice: Tenant disabled successfully index: create: Create tenant + enable: "Enable tenant %{tenant}" + enabled: Enabled new: title: New tenant + restore: + notice: Tenant enabled successfully update: notice: Tenant updated successfully homepage: diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 948c6cf5c..509748e57 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -1634,10 +1634,16 @@ es: form: use_subdomain: "Utiliza un subdominio en el dominio %{domain} para acceder a esta entidad" use_domain: "Utiliza un dominio distinto para acceder a esta entidad" + hide: + notice: Entidad deshabilitada correctamente index: create: Crear entidad + enable: "Habilitar entidad %{tenant}" + enabled: Habilitada new: title: Nueva entidad + restore: + notice: Entidad habilitada correctamente update: notice: Entidad actualizada correctamente homepage: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index af7d96cec..2fa509e36 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -284,7 +284,12 @@ namespace :admin do delete :cancel, on: :collection end - resources :tenants, except: [:show, :destroy] + resources :tenants, except: [:show, :destroy] do + member do + put :hide + put :restore + end + end end resolve "Milestone" do |milestone| diff --git a/db/migrate/20221203140136_add_hidden_at_to_tenants.rb b/db/migrate/20221203140136_add_hidden_at_to_tenants.rb new file mode 100644 index 000000000..5e2006313 --- /dev/null +++ b/db/migrate/20221203140136_add_hidden_at_to_tenants.rb @@ -0,0 +1,5 @@ +class AddHiddenAtToTenants < ActiveRecord::Migration[6.0] + def change + add_column :tenants, :hidden_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index f12f53061..be20fc451 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_11_20_123254) do +ActiveRecord::Schema.define(version: 2022_12_03_140136) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -1562,6 +1562,7 @@ ActiveRecord::Schema.define(version: 2022_11_20_123254) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "schema_type", default: 0, null: false + t.datetime "hidden_at" t.index ["name"], name: "index_tenants_on_name", unique: true t.index ["schema"], name: "index_tenants_on_schema", unique: true end diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb index a34ec08e6..956fed48c 100644 --- a/spec/models/abilities/administrator_spec.rb +++ b/spec/models/abilities/administrator_spec.rb @@ -181,6 +181,8 @@ describe Abilities::Administrator do it { should be_able_to :create, Tenant } it { should be_able_to :read, Tenant } it { should be_able_to :update, Tenant } + it { should be_able_to :hide, Tenant } + it { should be_able_to :restore, Tenant } it { should_not be_able_to :destroy, Tenant } context "administrators from other tenants" do @@ -193,6 +195,8 @@ describe Abilities::Administrator do it { should_not be_able_to :read, Tenant } it { should_not be_able_to :update, Tenant } it { should_not be_able_to :destroy, Tenant } + it { should_not be_able_to :hide, Tenant } + it { should_not be_able_to :restore, Tenant } end end end diff --git a/spec/models/tenant_spec.rb b/spec/models/tenant_spec.rb index b15dc7560..ba94f001a 100644 --- a/spec/models/tenant_spec.rb +++ b/spec/models/tenant_spec.rb @@ -117,6 +117,37 @@ describe Tenant do expect(Tenant.resolve_host("www.consul.dev")).to eq "www.consul.dev" end + it "raises an exception when accessing a hidden tenant using a subdomain" do + insert(:tenant, schema: "saturn", hidden_at: Time.current) + + expect { Tenant.resolve_host("saturn.consul.dev") }.to raise_exception(Apartment::TenantNotFound) + end + + it "raises an exception when accessing a hidden tenant using a domain" do + insert(:tenant, :domain, schema: "consul.dev", hidden_at: Time.current) + + expect { Tenant.resolve_host("consul.dev") }.to raise_exception(Apartment::TenantNotFound) + end + + it "raises an exception when accessing a hidden tenant using a domain starting with www" do + insert(:tenant, :domain, schema: "www.consul.dev", hidden_at: Time.current) + + expect { Tenant.resolve_host("www.consul.dev") }.to raise_exception(Apartment::TenantNotFound) + end + + it "raises an exception when accessing a hidden tenant with a domain and another tenant resolves to the same domain" do + insert(:tenant, :domain, schema: "saturn.consul.dev", hidden_at: Time.current) + insert(:tenant, schema: "saturn") + + expect { Tenant.resolve_host("saturn.consul.dev") }.to raise_exception(Apartment::TenantNotFound) + end + + it "ignores hidden tenants with nil as their schema" do + insert(:tenant, schema: nil, hidden_at: Time.current) + + expect(Tenant.resolve_host("consul.dev")).to be nil + end + context "multitenancy disabled" do before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) } @@ -318,6 +349,11 @@ describe Tenant do expect(build(:tenant, schema: "subdomainx")).not_to be_valid end + it "is not valid with the schema of an already existing hidden record" do + insert(:tenant, schema: "subdomainx", hidden_at: Time.current) + expect(build(:tenant, schema: "subdomainx")).not_to be_valid + end + it "is not valid with an excluded subdomain" do %w[mail public shared_extensions www].each do |subdomain| tenant.schema = subdomain @@ -345,6 +381,11 @@ describe Tenant do expect(build(:tenant, name: "Name X")).not_to be_valid end + it "is not valid with the name of an already existing hidden record" do + insert(:tenant, name: "Name X", hidden_at: Time.current) + expect(build(:tenant, name: "Name X")).not_to be_valid + end + context "Domain schema type" do before { tenant.schema_type = :domain } diff --git a/spec/system/admin/tenants_spec.rb b/spec/system/admin/tenants_spec.rb index 92096261b..38389dbe8 100644 --- a/spec/system/admin/tenants_spec.rb +++ b/spec/system/admin/tenants_spec.rb @@ -63,4 +63,43 @@ describe "Tenants", :admin, :seed_tenants do expect(page).to have_current_path root_path expect(page).to have_link "Sign in" end + + scenario "Hide and restore", :show_exceptions do + create(:tenant, schema: "moon", name: "Moon") + + visit admin_tenants_path + + within("tr", text: "moon") do + expect(page).to have_content "Yes" + + click_button "Enable tenant Moon" + + expect(page).to have_content "No" + expect(page).not_to have_link "moon.lvh.me" + end + + with_subdomain("moon") do + visit root_path + + expect(page).to have_title "Not found" + end + + visit admin_tenants_path + + within("tr", text: "moon") do + expect(page).to have_content "No" + + click_button "Enable tenant Moon" + + expect(page).to have_content "Yes" + expect(page).to have_link "moon.lvh.me" + end + + with_subdomain("moon") do + visit root_path + + expect(page).to have_link "Sign in" + expect(page).not_to have_title "Not found" + end + end end