Merge pull request #5050 from consul/hide_tenants
Make it possible to disable tenants
This commit is contained in:
@@ -335,6 +335,7 @@ Rails/SkipsModelValidations:
|
||||
ForbiddenMethods:
|
||||
- update_attribute
|
||||
Exclude:
|
||||
- app/models/tenant.rb
|
||||
- lib/acts_as_paranoid_aliases.rb
|
||||
|
||||
Rails/TimeZone:
|
||||
|
||||
@@ -3,9 +3,4 @@
|
||||
caption {
|
||||
@include element-invisible;
|
||||
}
|
||||
|
||||
[aria-pressed] {
|
||||
@include switch;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
6
app/assets/stylesheets/admin/toggle_switch.scss
Normal file
6
app/assets/stylesheets/admin/toggle_switch.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.admin .toggle-switch {
|
||||
[aria-pressed] {
|
||||
@include switch;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<%= render Admin::ActionComponent.new(:toggle_enabled, phase, options) %>
|
||||
<%= render Admin::ToggleSwitchComponent.new(action, phase, pressed: enabled?, **options) %>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Admin::BudgetPhases::ToggleEnabledComponent < ApplicationComponent
|
||||
attr_reader :phase
|
||||
delegate :enabled?, to: :phase
|
||||
|
||||
def initialize(phase)
|
||||
@phase = phase
|
||||
@@ -8,20 +9,14 @@ class Admin::BudgetPhases::ToggleEnabledComponent < ApplicationComponent
|
||||
private
|
||||
|
||||
def options
|
||||
{
|
||||
text: text,
|
||||
method: :patch,
|
||||
remote: true,
|
||||
"aria-label": t("admin.budgets.edit.enable_phase", phase: phase.name),
|
||||
"aria-pressed": phase.enabled?
|
||||
}
|
||||
{ "aria-label": t("admin.budgets.edit.enable_phase", phase: phase.name) }
|
||||
end
|
||||
|
||||
def text
|
||||
if phase.enabled?
|
||||
t("shared.yes")
|
||||
def action
|
||||
if enabled?
|
||||
:disable
|
||||
else
|
||||
t("shared.no")
|
||||
:enable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,20 +8,14 @@
|
||||
<th><%= attribute_name(:name) %></th>
|
||||
<th><%= attribute_name(:schema) %></th>
|
||||
<th><%= attribute_name(:url) %></th>
|
||||
<th><%= t("admin.tenants.index.enabled") %></th>
|
||||
<th><%= t("admin.shared.actions") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% @tenants.each do |tenant| %>
|
||||
<tr id="<%= dom_id(tenant) %>">
|
||||
<td><%= tenant.name %></td>
|
||||
<td><%= tenant.schema %></td>
|
||||
<td><%= link_to tenant.host, root_url(host: tenant.host) %></td>
|
||||
<td>
|
||||
<%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) %>
|
||||
</td>
|
||||
</tr>
|
||||
<%= render Admin::Tenants::RowComponent.new(tenant) %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
9
app/components/admin/tenants/row_component.html.erb
Normal file
9
app/components/admin/tenants/row_component.html.erb
Normal file
@@ -0,0 +1,9 @@
|
||||
<tr id="<%= dom_id(tenant) %>">
|
||||
<td><%= tenant.name %></td>
|
||||
<td><%= tenant.schema %></td>
|
||||
<td><%= link_to_unless tenant.hidden?, tenant.host, root_url(host: tenant.host) %></td>
|
||||
<td><%= render Admin::Tenants::ToggleHiddenComponent.new(tenant) %></td>
|
||||
<td>
|
||||
<%= render Admin::TableActionsComponent.new(tenant, actions: [:edit]) %>
|
||||
</td>
|
||||
</tr>
|
||||
7
app/components/admin/tenants/row_component.rb
Normal file
7
app/components/admin/tenants/row_component.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Admin::Tenants::RowComponent < ApplicationComponent
|
||||
attr_reader :tenant
|
||||
|
||||
def initialize(tenant)
|
||||
@tenant = tenant
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
<%= render Admin::ToggleSwitchComponent.new(action, tenant, pressed: enabled?, **options) %>
|
||||
28
app/components/admin/tenants/toggle_hidden_component.rb
Normal file
28
app/components/admin/tenants/toggle_hidden_component.rb
Normal file
@@ -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
|
||||
1
app/components/admin/toggle_switch_component.html.erb
Normal file
1
app/components/admin/toggle_switch_component.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render Admin::ActionComponent.new(action, record, **default_options.merge(options)) %>
|
||||
31
app/components/admin/toggle_switch_component.rb
Normal file
31
app/components/admin/toggle_switch_component.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class Admin::ToggleSwitchComponent < ApplicationComponent
|
||||
attr_reader :action, :record, :pressed, :options
|
||||
alias_method :pressed?, :pressed
|
||||
|
||||
def initialize(action, record, pressed:, **options)
|
||||
@action = action
|
||||
@record = record
|
||||
@pressed = pressed
|
||||
@options = options
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def text
|
||||
if pressed?
|
||||
t("shared.yes")
|
||||
else
|
||||
t("shared.no")
|
||||
end
|
||||
end
|
||||
|
||||
def default_options
|
||||
{
|
||||
text: text,
|
||||
method: :patch,
|
||||
remote: true,
|
||||
"aria-pressed": pressed?,
|
||||
form_class: "toggle-switch"
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ module Admin::BudgetPhasesActions
|
||||
include ImageAttributes
|
||||
|
||||
before_action :load_budget
|
||||
before_action :load_phase, only: [:edit, :update, :toggle_enabled]
|
||||
before_action :load_phase, only: [:edit, :update, :enable, :disable]
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -20,12 +20,21 @@ module Admin::BudgetPhasesActions
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_enabled
|
||||
@phase.update!(enabled: !@phase.enabled)
|
||||
def enable
|
||||
@phase.update!(enabled: true)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to phases_index, notice: t("flash.actions.save_changes.notice") }
|
||||
format.js
|
||||
format.js { render template: "admin/budgets_wizard/phases/toggle_enabled" }
|
||||
end
|
||||
end
|
||||
|
||||
def disable
|
||||
@phase.update!(enabled: false)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to phases_index, notice: t("flash.actions.save_changes.notice") }
|
||||
format.js { render template: "admin/budgets_wizard/phases/toggle_enabled" }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -30,7 +42,11 @@ class Tenant < ApplicationRecord
|
||||
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)
|
||||
if find_by_domain(schema)
|
||||
raise Apartment::TenantNotFound
|
||||
else
|
||||
schema
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -133,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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<%= render template: "admin/budgets_wizard/phases/toggle_enabled" %>
|
||||
@@ -1,4 +1,5 @@
|
||||
var replacement = $("<%= j render Admin::BudgetPhases::ToggleEnabledComponent.new(@phase) %>");
|
||||
var form = $("#" + replacement.find("[type='submit']").attr("id")).closest("form");
|
||||
var form = $("#<%= dom_id(@phase) %> .toggle-switch");
|
||||
|
||||
form.html(replacement.html()).find("[type='submit']").focus();
|
||||
form.replaceWith(replacement);
|
||||
replacement.find("[type='submit']").focus();
|
||||
|
||||
4
app/views/admin/tenants/toggle_enabled.js.erb
Normal file
4
app/views/admin/tenants/toggle_enabled.js.erb
Normal file
@@ -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();
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -70,7 +70,10 @@ namespace :admin do
|
||||
end
|
||||
|
||||
resources :budget_phases, only: [:edit, :update] do
|
||||
member { patch :toggle_enabled }
|
||||
member do
|
||||
patch :enable
|
||||
patch :disable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,7 +84,10 @@ namespace :admin do
|
||||
end
|
||||
|
||||
resources :phases, as: "budget_phases", only: [:index, :edit, :update] do
|
||||
member { patch :toggle_enabled }
|
||||
member do
|
||||
patch :enable
|
||||
patch :disable
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -278,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|
|
||||
|
||||
5
db/migrate/20221203140136_add_hidden_at_to_tenants.rb
Normal file
5
db/migrate/20221203140136_add_hidden_at_to_tenants.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddHiddenAtToTenants < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :tenants, :hidden_at, :datetime
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -86,10 +86,10 @@ describe Tenant do
|
||||
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn"
|
||||
end
|
||||
|
||||
it "returns nil when a domain is accessed as a subdomain" do
|
||||
it "raises an exception when a domain is accessed as a subdomain" do
|
||||
insert(:tenant, :domain, schema: "saturn.dev")
|
||||
|
||||
expect(Tenant.resolve_host("saturn.dev.consul.dev")).to be nil
|
||||
expect { Tenant.resolve_host("saturn.dev.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
|
||||
end
|
||||
|
||||
it "returns nested subdomains when there's a subdomain-type tenant with nested subdomains" do
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user