mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 03:07:52 +01:00
Add custom domains (#314)
This commit is contained in:
committed by
GitHub
parent
d47c70f576
commit
d17b45c5c4
@@ -1,4 +1,7 @@
|
||||
require 'uri'
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include ApplicationHelper
|
||||
include Pundit::Authorization
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||
@@ -16,24 +19,14 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def load_tenant_data
|
||||
if Rails.application.multi_tenancy?
|
||||
return if request.subdomain.blank? or RESERVED_SUBDOMAINS.include?(request.subdomain)
|
||||
current_tenant = get_tenant_from_request(request)
|
||||
|
||||
# Load the current tenant based on subdomain
|
||||
current_tenant = Tenant.find_by(subdomain: request.subdomain)
|
||||
if current_tenant.status == "pending" and controller_name != "confirmation" and action_name != "show"
|
||||
redirect_to pending_tenant_path; return
|
||||
end
|
||||
|
||||
if current_tenant.status == "pending" and controller_name != "confirmation" and action_name != "show"
|
||||
redirect_to pending_tenant_path; return
|
||||
end
|
||||
|
||||
if current_tenant.status == "blocked"
|
||||
redirect_to blocked_tenant_path; return
|
||||
end
|
||||
|
||||
redirect_to showcase_url unless current_tenant
|
||||
else
|
||||
# Load the one and only tenant
|
||||
current_tenant = Tenant.first
|
||||
if current_tenant.status == "blocked"
|
||||
redirect_to blocked_tenant_path; return
|
||||
end
|
||||
|
||||
return unless current_tenant
|
||||
|
||||
@@ -61,10 +61,10 @@ class OAuthsController < ApplicationController
|
||||
|
||||
if user
|
||||
oauth_token = user.generate_oauth_token
|
||||
redirect_to add_subdomain_to(method(:o_auth_sign_in_from_oauth_token_url), nil, {user_id: user.id, token: oauth_token})
|
||||
redirect_to get_url_for(method(:o_auth_sign_in_from_oauth_token_url), options: { user_id: user.id, token: oauth_token })
|
||||
else
|
||||
flash[:alert] = I18n.t('errors.o_auth_login_error', name: @o_auth.name)
|
||||
redirect_to add_subdomain_to(method(:new_user_session_url))
|
||||
redirect_to get_url_for(method(:new_user_session_url))
|
||||
end
|
||||
|
||||
elsif reason == 'test'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'httparty'
|
||||
|
||||
class TenantsController < ApplicationController
|
||||
include ApplicationHelper
|
||||
|
||||
@@ -66,6 +68,23 @@ class TenantsController < ApplicationController
|
||||
@tenant = Current.tenant_or_raise!
|
||||
authorize @tenant
|
||||
|
||||
# If the custom domain has changed, we need to provision SSL certificate
|
||||
custom_domain_response = AddCustomDomainWorkflow.new(
|
||||
new_custom_domain: params[:tenant][:custom_domain],
|
||||
current_custom_domain: @tenant.custom_domain
|
||||
).run
|
||||
|
||||
if custom_domain_response == false
|
||||
render json: {
|
||||
error: "Error adding custom domain"
|
||||
}, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
# Since custom_domain is unique at db level, we need to set it to nil if it is blank
|
||||
# to avoid unique constraint violation
|
||||
params[:tenant][:custom_domain] = nil if params[:tenant][:custom_domain].blank?
|
||||
|
||||
if @tenant.update(tenant_update_params)
|
||||
render json: @tenant
|
||||
else
|
||||
|
||||
@@ -28,10 +28,44 @@ module ApplicationHelper
|
||||
end
|
||||
end
|
||||
|
||||
def add_subdomain_to(url_helper, resource=nil, options={})
|
||||
options[:subdomain] = Current.tenant_or_raise!.subdomain if Rails.application.multi_tenancy?
|
||||
options[:host] = Rails.application.base_url
|
||||
def get_url_for(url_helper, resource: nil, disallow_custom_domain: false, options: {})
|
||||
custom_domain = Current.tenant.custom_domain
|
||||
|
||||
if Rails.application.multi_tenancy? && (custom_domain.blank? || disallow_custom_domain)
|
||||
options[:subdomain] = Current.tenant.subdomain
|
||||
end
|
||||
|
||||
if custom_domain.blank? || disallow_custom_domain
|
||||
options[:host] = Rails.application.base_url
|
||||
else
|
||||
options[:host] = custom_domain
|
||||
end
|
||||
|
||||
if Rails.application.base_url.include?('https')
|
||||
options[:protocol] = 'https'
|
||||
else
|
||||
options[:protocol] = 'http'
|
||||
end
|
||||
|
||||
resource ? url_helper.call(resource, options) : url_helper.call(options)
|
||||
end
|
||||
|
||||
def get_tenant_from_request(request)
|
||||
if Rails.application.multi_tenancy?
|
||||
request_host_splitted = request.host.split('.')
|
||||
app_host_splitted = URI.parse(Rails.application.base_url).host.split('.')
|
||||
|
||||
if app_host_splitted.join('.') == request_host_splitted.last(app_host_splitted.length).join('.')
|
||||
return if request.subdomain.blank? or RESERVED_SUBDOMAINS.include?(request.subdomain)
|
||||
|
||||
tenant = Tenant.find_by(subdomain: request.subdomain)
|
||||
else
|
||||
tenant = Tenant.find_by(custom_domain: request.host)
|
||||
end
|
||||
else
|
||||
tenant = Tenant.first
|
||||
end
|
||||
|
||||
tenant
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,6 +50,7 @@ interface UpdateTenantParams {
|
||||
siteLogo?: string;
|
||||
tenantSetting?: ITenantSetting;
|
||||
locale?: string;
|
||||
customDomain?: string;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ export const updateTenant = ({
|
||||
siteLogo = null,
|
||||
tenantSetting = null,
|
||||
locale = null,
|
||||
customDomain = null,
|
||||
authenticityToken,
|
||||
}: UpdateTenantParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(tenantUpdateStart());
|
||||
@@ -65,7 +67,8 @@ export const updateTenant = ({
|
||||
const tenant = Object.assign({},
|
||||
siteName !== null ? { site_name: siteName } : null,
|
||||
siteLogo !== null ? { site_logo: siteLogo } : null,
|
||||
locale !== null ? { locale } : null
|
||||
locale !== null ? { locale } : null,
|
||||
customDomain !== null ? { custom_domain: customDomain } : null,
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import { DangerText, SmallMutedText } from '../../common/CustomTexts';
|
||||
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
||||
import IBoardJSON from '../../../interfaces/json/IBoard';
|
||||
import ActionLink from '../../common/ActionLink';
|
||||
import { LearnMoreIcon } from '../../common/Icons';
|
||||
|
||||
export interface ISiteSettingsGeneralForm {
|
||||
siteName: string;
|
||||
@@ -27,6 +29,7 @@ export interface ISiteSettingsGeneralForm {
|
||||
showVoteButtonInBoard: boolean;
|
||||
showPoweredBy: boolean;
|
||||
rootBoardId?: string;
|
||||
customDomain?: string;
|
||||
showRoadmapInHeader: boolean;
|
||||
collapseBoardsInHeader: string;
|
||||
}
|
||||
@@ -45,6 +48,7 @@ interface Props {
|
||||
brandDisplaySetting: string,
|
||||
locale: string,
|
||||
rootBoardId: number,
|
||||
customDomain: string,
|
||||
showRoadmapInHeader: boolean,
|
||||
collapseBoardsInHeader: string,
|
||||
showVoteCount: boolean,
|
||||
@@ -66,7 +70,8 @@ const GeneralSiteSettingsP = ({
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isSubmitSuccessful, errors }
|
||||
formState: { isDirty, isSubmitSuccessful, errors },
|
||||
watch,
|
||||
} = useForm<ISiteSettingsGeneralForm>({
|
||||
defaultValues: {
|
||||
siteName: originForm.siteName,
|
||||
@@ -77,6 +82,7 @@ const GeneralSiteSettingsP = ({
|
||||
showVoteButtonInBoard: originForm.showVoteButtonInBoard,
|
||||
showPoweredBy: originForm.showPoweredBy,
|
||||
rootBoardId: originForm.rootBoardId,
|
||||
customDomain: originForm.customDomain,
|
||||
showRoadmapInHeader: originForm.showRoadmapInHeader,
|
||||
collapseBoardsInHeader: originForm.collapseBoardsInHeader,
|
||||
},
|
||||
@@ -89,6 +95,7 @@ const GeneralSiteSettingsP = ({
|
||||
data.brandDisplaySetting,
|
||||
data.locale,
|
||||
Number(data.rootBoardId),
|
||||
data.customDomain,
|
||||
data.showRoadmapInHeader,
|
||||
data.collapseBoardsInHeader,
|
||||
data.showVoteCount,
|
||||
@@ -101,6 +108,8 @@ const GeneralSiteSettingsP = ({
|
||||
});
|
||||
};
|
||||
|
||||
const customDomain = watch('customDomain');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
@@ -186,6 +195,29 @@ const GeneralSiteSettingsP = ({
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="formGroup">
|
||||
<label htmlFor="customDomain">{ getLabel('tenant', 'custom_domain') }</label>
|
||||
<input
|
||||
{...register('customDomain')}
|
||||
id="customDomain"
|
||||
className="formControl"
|
||||
/>
|
||||
{
|
||||
originForm.customDomain !== customDomain && customDomain !== '' &&
|
||||
<div style={{marginTop: 16}}>
|
||||
<SmallMutedText>
|
||||
{ I18n.t('site_settings.general.custom_domain_help', { domain: customDomain }) }
|
||||
</SmallMutedText>
|
||||
<ActionLink
|
||||
onClick={() => window.open('https://docs.astuto.io/custom-domain', '_blank')}
|
||||
icon={<LearnMoreIcon />}
|
||||
>
|
||||
{I18n.t('site_settings.general.custom_domain_learn_more')}
|
||||
</ActionLink>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<h4>{ I18n.t('site_settings.general.subtitle_header') }</h4>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
brandDisplaySetting: TenantSettingBrandDisplay,
|
||||
locale: string,
|
||||
rootBoardId: number,
|
||||
customDomain: string,
|
||||
showRoadmapInHeader: boolean,
|
||||
collapseBoardsInHeader: TenantSettingCollapseBoardsInHeader,
|
||||
showVoteCount: boolean,
|
||||
@@ -37,6 +38,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
collapse_boards_in_header: collapseBoardsInHeader,
|
||||
},
|
||||
locale,
|
||||
customDomain,
|
||||
authenticityToken,
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ interface ITenant {
|
||||
siteName: string;
|
||||
siteLogo: string;
|
||||
locale: string;
|
||||
customDomain?: string;
|
||||
}
|
||||
|
||||
export default ITenant;
|
||||
@@ -4,6 +4,7 @@ interface ITenantJSON {
|
||||
site_logo: string;
|
||||
brand_display_setting: string;
|
||||
locale: string;
|
||||
custom_domain?: string;
|
||||
}
|
||||
|
||||
export default ITenantJSON;
|
||||
@@ -27,9 +27,9 @@ class OAuth < ApplicationRecord
|
||||
# for this reason, we don't preprend tenant subdomain
|
||||
# but rather use the "login" subdomain
|
||||
if self.is_default?
|
||||
o_auth_callback_url(id, host: Rails.application.base_url, subdomain: "login")
|
||||
get_url_for(method(:o_auth_callback_url), resource: id, disallow_custom_domain: true, options: { subdomain: "login", host: Rails.application.base_url })
|
||||
else
|
||||
add_subdomain_to(method(:o_auth_callback_url), id)
|
||||
get_url_for(method(:o_auth_callback_url), resource: id, disallow_custom_domain: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class Tenant < ApplicationRecord
|
||||
validates :site_name, presence: true
|
||||
validates :subdomain, presence: true, uniqueness: true
|
||||
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
|
||||
validates :custom_domain, format: { with: /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/ }, uniqueness: true, allow_blank: true, allow_nil: true
|
||||
|
||||
accepts_nested_attributes_for :tenant_setting, update_only: true
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ class TenantPolicy < ApplicationPolicy
|
||||
|
||||
def permitted_attributes_for_update
|
||||
if user.admin?
|
||||
[:site_name, :site_logo, :locale]
|
||||
[:site_name, :site_logo, :locale, :custom_domain]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
<p>
|
||||
<%= link_to t('mailers.devise.confirmation_instructions.action'),
|
||||
add_subdomain_to(method(:confirmation_url), @resource, { confirmation_token: @token })
|
||||
get_url_for(method(:confirmation_url), resource: @resource, options: { confirmation_token: @token })
|
||||
%>
|
||||
</p>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<p>
|
||||
<%= link_to t('mailers.devise.reset_password.action'),
|
||||
add_subdomain_to(method(:edit_password_url), @resource, { reset_password_token: @token })
|
||||
get_url_for(method(:edit_password_url), resource: @resource, options: { reset_password_token: @token })
|
||||
%>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
|
||||
<p>
|
||||
<%= link_to 'Unlock my account',
|
||||
add_subdomain_to(method(:unlock_url), @resource, { unlock_token: @token })
|
||||
get_url_for(method(:unlock_url), resource: @resource, options: { unlock_token: @token })
|
||||
%>
|
||||
</p>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin: 16px auto; text-align: center;">
|
||||
<a href="<%= add_subdomain_to(method(:root_url)) %>">
|
||||
<a href="<%= get_url_for(method(:root_url)) %>">
|
||||
<%= image_tag('https://raw.githubusercontent.com/astuto/astuto-assets/main/logo-64.png', alt: 'Astuto Logo', size: 64) %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
showVoteButtonInBoard: @tenant_setting.show_vote_button_in_board,
|
||||
showPoweredBy: @tenant_setting.show_powered_by,
|
||||
rootBoardId: @tenant_setting.root_board_id.to_s,
|
||||
customDomain: @tenant.custom_domain,
|
||||
showRoadmapInHeader: @tenant_setting.show_roadmap_in_header,
|
||||
collapseBoardsInHeader: @tenant_setting.collapse_boards_in_header,
|
||||
locale: @tenant.locale
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%=
|
||||
t(
|
||||
'mailers.user.unsubscribe_from_post_html',
|
||||
href: link_to(t('mailers.user.unsubscribe_link'), add_subdomain_to(method(:post_url), post))
|
||||
href: link_to(t('mailers.user.unsubscribe_link'), get_url_for(method(:post_url), resource: post))
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%=
|
||||
t(
|
||||
'mailers.user.unsubscribe_from_site_html',
|
||||
href: link_to(t('mailers.user.unsubscribe_link'), add_subdomain_to(method(:edit_user_registration_url)))
|
||||
href: link_to(t('mailers.user.unsubscribe_link'), get_url_for(method(:edit_user_registration_url)))
|
||||
).html_safe
|
||||
%>
|
||||
</p>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
||||
|
||||
<p>
|
||||
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @comment.post) %>
|
||||
<%= link_to t('mailers.user.learn_more'), get_url_for(method(:post_url), resource: @comment.post) %>
|
||||
</p>
|
||||
|
||||
<%= render 'user_mailer/closing' %>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @post) %>
|
||||
<%= link_to t('mailers.user.learn_more'), get_url_for(method(:post_url), resource: @post) %>
|
||||
</p>
|
||||
|
||||
<%= render 'user_mailer/closing' %>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
||||
|
||||
<p>
|
||||
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @comment.post) %>
|
||||
<%= link_to t('mailers.user.learn_more'), get_url_for(method(:post_url), resource: @comment.post) %>
|
||||
</p>
|
||||
|
||||
<%= render 'user_mailer/closing' %>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
||||
|
||||
<p>
|
||||
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @comment.post) %>
|
||||
<%= link_to t('mailers.user.learn_more'), get_url_for(method(:post_url), resource: @comment.post) %>
|
||||
</p>
|
||||
|
||||
<%= render 'user_mailer/closing' %>
|
||||
|
||||
48
app/workflows/add_custom_domain_workflow.rb
Normal file
48
app/workflows/add_custom_domain_workflow.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class AddCustomDomainWorkflow
|
||||
include HTTParty
|
||||
|
||||
attr_accessor :new_custom_domain, :current_custom_domain
|
||||
|
||||
def initialize(new_custom_domain: "", current_custom_domain: "")
|
||||
@new_custom_domain = new_custom_domain
|
||||
@current_custom_domain = current_custom_domain
|
||||
end
|
||||
|
||||
def make_request(method, domain)
|
||||
return unless method == "POST" || method == "DELETE"
|
||||
|
||||
HTTParty.send(
|
||||
method.downcase,
|
||||
ENV["ASTUTO_CNAME_API_URL"],
|
||||
headers: {
|
||||
"api_key" => ENV["ASTUTO_CNAME_API_KEY"],
|
||||
"Accept" => "application/json",
|
||||
},
|
||||
query: { "domain" => domain.downcase }
|
||||
)
|
||||
end
|
||||
|
||||
def run
|
||||
return true unless Rails.application.multi_tenancy?
|
||||
return true if @new_custom_domain == @current_custom_domain
|
||||
return true if Tenant.exists?(custom_domain: @new_custom_domain)
|
||||
|
||||
begin
|
||||
# Add new custom domain...
|
||||
if @new_custom_domain.present?
|
||||
response = make_request("POST", @new_custom_domain)
|
||||
|
||||
return false unless response.success?
|
||||
end
|
||||
|
||||
# ... and remove the current one
|
||||
if @current_custom_domain.present?
|
||||
make_request("DELETE", @current_custom_domain)
|
||||
end
|
||||
|
||||
return true
|
||||
rescue => e
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,8 @@ Rails.application.configure do
|
||||
|
||||
config.hosts << ".localhost:3000"
|
||||
config.hosts << ".lvh.me:3000" # used to test oauth strategies in development
|
||||
config.hosts << ".ngrok-free.app"
|
||||
config.hosts << ".riggraz.dev"
|
||||
|
||||
# 0 if using localhost, 1 if using lvh.me
|
||||
config.action_dispatch.tld_length = 0
|
||||
|
||||
@@ -116,6 +116,7 @@ en:
|
||||
site_logo: 'Site logo'
|
||||
subdomain: 'Subdomain'
|
||||
locale: 'Language'
|
||||
custom_domain: 'Custom domain'
|
||||
tenant_setting:
|
||||
brand_display: 'Display'
|
||||
show_vote_count: 'Show vote count to users'
|
||||
|
||||
@@ -158,6 +158,8 @@ en:
|
||||
collapse_boards_in_header_no_collapse: 'Never'
|
||||
collapse_boards_in_header_always_collapse: 'Always'
|
||||
subtitle_visibility: 'Visibility'
|
||||
custom_domain_help: 'In your DNS settings, add a CNAME record pointing "%{domain}" to "cname.astuto.io"'
|
||||
custom_domain_learn_more: 'Learn how to configure a custom domain'
|
||||
show_vote_count_help: 'If you enable this setting, users will be able to see the vote count of posts. This may incentivize users to vote on already popular posts, leading to a snowball effect.'
|
||||
show_vote_button_in_board_help: 'If you enable this setting, users will be able to vote posts from the board page. This may incentivize users to vote on more posts, leading to a higher number of votes but of lower significance.'
|
||||
boards:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class ChangeTenantCustomUrlColumn < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
rename_column :tenants, :custom_url, :custom_domain
|
||||
add_index :tenants, :custom_domain, unique: true
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2024_03_03_103945) do
|
||||
ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@@ -153,10 +153,11 @@ ActiveRecord::Schema.define(version: 2024_03_03_103945) do
|
||||
t.string "site_logo"
|
||||
t.string "subdomain", null: false
|
||||
t.string "locale", default: "en"
|
||||
t.string "custom_url"
|
||||
t.string "custom_domain"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.integer "status"
|
||||
t.index ["custom_domain"], name: "index_tenants_on_custom_domain", unique: true
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
|
||||
@@ -4,6 +4,7 @@ FactoryBot.define do
|
||||
site_logo { "" }
|
||||
sequence(:subdomain) { |n| "mysubdomain#{n}" }
|
||||
locale { "en" }
|
||||
custom_url { "" }
|
||||
custom_domain { nil }
|
||||
status { "active" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,4 +35,38 @@ RSpec.describe Tenant, type: :model do
|
||||
tenant.subdomain = tenant2.subdomain
|
||||
expect(tenant).to be_invalid
|
||||
end
|
||||
|
||||
it 'may have a valid custom domain' do
|
||||
expect(tenant.custom_domain).to be_nil
|
||||
|
||||
tenant.custom_domain = ''
|
||||
expect(tenant).to be_valid
|
||||
|
||||
tenant.custom_domain = 'example.com'
|
||||
expect(tenant).to be_valid
|
||||
|
||||
tenant.custom_domain = 'subdomain.example.com'
|
||||
expect(tenant).to be_valid
|
||||
|
||||
tenant.custom_domain = 'sub.subdomain.example.com'
|
||||
expect(tenant).to be_valid
|
||||
|
||||
tenant.custom_domain = 'com'
|
||||
expect(tenant).to be_invalid
|
||||
|
||||
tenant.custom_domain = 'https://example.com'
|
||||
expect(tenant).to be_invalid
|
||||
|
||||
tenant.custom_domain = 'example.com/sub'
|
||||
expect(tenant).to be_invalid
|
||||
|
||||
tenant.custom_domain = 'example.com.'
|
||||
expect(tenant).to be_invalid
|
||||
|
||||
tenant.custom_domain = 'example..com'
|
||||
expect(tenant).to be_invalid
|
||||
|
||||
tenant.custom_domain = '.example.com'
|
||||
expect(tenant).to be_invalid
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user