mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27: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
|
class ApplicationController < ActionController::Base
|
||||||
|
include ApplicationHelper
|
||||||
include Pundit::Authorization
|
include Pundit::Authorization
|
||||||
|
|
||||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||||
@@ -16,24 +19,14 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_tenant_data
|
def load_tenant_data
|
||||||
if Rails.application.multi_tenancy?
|
current_tenant = get_tenant_from_request(request)
|
||||||
return if request.subdomain.blank? or RESERVED_SUBDOMAINS.include?(request.subdomain)
|
|
||||||
|
|
||||||
# Load the current tenant based on subdomain
|
if current_tenant.status == "pending" and controller_name != "confirmation" and action_name != "show"
|
||||||
current_tenant = Tenant.find_by(subdomain: request.subdomain)
|
redirect_to pending_tenant_path; return
|
||||||
|
end
|
||||||
|
|
||||||
if current_tenant.status == "pending" and controller_name != "confirmation" and action_name != "show"
|
if current_tenant.status == "blocked"
|
||||||
redirect_to pending_tenant_path; return
|
redirect_to blocked_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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless current_tenant
|
return unless current_tenant
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ class OAuthsController < ApplicationController
|
|||||||
|
|
||||||
if user
|
if user
|
||||||
oauth_token = user.generate_oauth_token
|
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
|
else
|
||||||
flash[:alert] = I18n.t('errors.o_auth_login_error', name: @o_auth.name)
|
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
|
end
|
||||||
|
|
||||||
elsif reason == 'test'
|
elsif reason == 'test'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require 'httparty'
|
||||||
|
|
||||||
class TenantsController < ApplicationController
|
class TenantsController < ApplicationController
|
||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
|
|
||||||
@@ -66,6 +68,23 @@ class TenantsController < ApplicationController
|
|||||||
@tenant = Current.tenant_or_raise!
|
@tenant = Current.tenant_or_raise!
|
||||||
authorize @tenant
|
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)
|
if @tenant.update(tenant_update_params)
|
||||||
render json: @tenant
|
render json: @tenant
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -28,10 +28,44 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_subdomain_to(url_helper, resource=nil, options={})
|
def get_url_for(url_helper, resource: nil, disallow_custom_domain: false, options: {})
|
||||||
options[:subdomain] = Current.tenant_or_raise!.subdomain if Rails.application.multi_tenancy?
|
custom_domain = Current.tenant.custom_domain
|
||||||
options[:host] = Rails.application.base_url
|
|
||||||
|
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)
|
resource ? url_helper.call(resource, options) : url_helper.call(options)
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ interface UpdateTenantParams {
|
|||||||
siteLogo?: string;
|
siteLogo?: string;
|
||||||
tenantSetting?: ITenantSetting;
|
tenantSetting?: ITenantSetting;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
customDomain?: string;
|
||||||
authenticityToken: string;
|
authenticityToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export const updateTenant = ({
|
|||||||
siteLogo = null,
|
siteLogo = null,
|
||||||
tenantSetting = null,
|
tenantSetting = null,
|
||||||
locale = null,
|
locale = null,
|
||||||
|
customDomain = null,
|
||||||
authenticityToken,
|
authenticityToken,
|
||||||
}: UpdateTenantParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
}: UpdateTenantParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
dispatch(tenantUpdateStart());
|
dispatch(tenantUpdateStart());
|
||||||
@@ -65,7 +67,8 @@ export const updateTenant = ({
|
|||||||
const tenant = Object.assign({},
|
const tenant = Object.assign({},
|
||||||
siteName !== null ? { site_name: siteName } : null,
|
siteName !== null ? { site_name: siteName } : null,
|
||||||
siteLogo !== null ? { site_logo: siteLogo } : null,
|
siteLogo !== null ? { site_logo: siteLogo } : null,
|
||||||
locale !== null ? { locale } : null
|
locale !== null ? { locale } : null,
|
||||||
|
customDomain !== null ? { custom_domain: customDomain } : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import { DangerText, SmallMutedText } from '../../common/CustomTexts';
|
import { DangerText, SmallMutedText } from '../../common/CustomTexts';
|
||||||
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
||||||
import IBoardJSON from '../../../interfaces/json/IBoard';
|
import IBoardJSON from '../../../interfaces/json/IBoard';
|
||||||
|
import ActionLink from '../../common/ActionLink';
|
||||||
|
import { LearnMoreIcon } from '../../common/Icons';
|
||||||
|
|
||||||
export interface ISiteSettingsGeneralForm {
|
export interface ISiteSettingsGeneralForm {
|
||||||
siteName: string;
|
siteName: string;
|
||||||
@@ -27,6 +29,7 @@ export interface ISiteSettingsGeneralForm {
|
|||||||
showVoteButtonInBoard: boolean;
|
showVoteButtonInBoard: boolean;
|
||||||
showPoweredBy: boolean;
|
showPoweredBy: boolean;
|
||||||
rootBoardId?: string;
|
rootBoardId?: string;
|
||||||
|
customDomain?: string;
|
||||||
showRoadmapInHeader: boolean;
|
showRoadmapInHeader: boolean;
|
||||||
collapseBoardsInHeader: string;
|
collapseBoardsInHeader: string;
|
||||||
}
|
}
|
||||||
@@ -45,6 +48,7 @@ interface Props {
|
|||||||
brandDisplaySetting: string,
|
brandDisplaySetting: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
rootBoardId: number,
|
rootBoardId: number,
|
||||||
|
customDomain: string,
|
||||||
showRoadmapInHeader: boolean,
|
showRoadmapInHeader: boolean,
|
||||||
collapseBoardsInHeader: string,
|
collapseBoardsInHeader: string,
|
||||||
showVoteCount: boolean,
|
showVoteCount: boolean,
|
||||||
@@ -66,7 +70,8 @@ const GeneralSiteSettingsP = ({
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty, isSubmitSuccessful, errors }
|
formState: { isDirty, isSubmitSuccessful, errors },
|
||||||
|
watch,
|
||||||
} = useForm<ISiteSettingsGeneralForm>({
|
} = useForm<ISiteSettingsGeneralForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
siteName: originForm.siteName,
|
siteName: originForm.siteName,
|
||||||
@@ -77,6 +82,7 @@ const GeneralSiteSettingsP = ({
|
|||||||
showVoteButtonInBoard: originForm.showVoteButtonInBoard,
|
showVoteButtonInBoard: originForm.showVoteButtonInBoard,
|
||||||
showPoweredBy: originForm.showPoweredBy,
|
showPoweredBy: originForm.showPoweredBy,
|
||||||
rootBoardId: originForm.rootBoardId,
|
rootBoardId: originForm.rootBoardId,
|
||||||
|
customDomain: originForm.customDomain,
|
||||||
showRoadmapInHeader: originForm.showRoadmapInHeader,
|
showRoadmapInHeader: originForm.showRoadmapInHeader,
|
||||||
collapseBoardsInHeader: originForm.collapseBoardsInHeader,
|
collapseBoardsInHeader: originForm.collapseBoardsInHeader,
|
||||||
},
|
},
|
||||||
@@ -89,6 +95,7 @@ const GeneralSiteSettingsP = ({
|
|||||||
data.brandDisplaySetting,
|
data.brandDisplaySetting,
|
||||||
data.locale,
|
data.locale,
|
||||||
Number(data.rootBoardId),
|
Number(data.rootBoardId),
|
||||||
|
data.customDomain,
|
||||||
data.showRoadmapInHeader,
|
data.showRoadmapInHeader,
|
||||||
data.collapseBoardsInHeader,
|
data.collapseBoardsInHeader,
|
||||||
data.showVoteCount,
|
data.showVoteCount,
|
||||||
@@ -101,6 +108,8 @@ const GeneralSiteSettingsP = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customDomain = watch('customDomain');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
@@ -186,6 +195,29 @@ const GeneralSiteSettingsP = ({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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 />
|
<br />
|
||||||
<h4>{ I18n.t('site_settings.general.subtitle_header') }</h4>
|
<h4>{ I18n.t('site_settings.general.subtitle_header') }</h4>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||||||
brandDisplaySetting: TenantSettingBrandDisplay,
|
brandDisplaySetting: TenantSettingBrandDisplay,
|
||||||
locale: string,
|
locale: string,
|
||||||
rootBoardId: number,
|
rootBoardId: number,
|
||||||
|
customDomain: string,
|
||||||
showRoadmapInHeader: boolean,
|
showRoadmapInHeader: boolean,
|
||||||
collapseBoardsInHeader: TenantSettingCollapseBoardsInHeader,
|
collapseBoardsInHeader: TenantSettingCollapseBoardsInHeader,
|
||||||
showVoteCount: boolean,
|
showVoteCount: boolean,
|
||||||
@@ -37,6 +38,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||||||
collapse_boards_in_header: collapseBoardsInHeader,
|
collapse_boards_in_header: collapseBoardsInHeader,
|
||||||
},
|
},
|
||||||
locale,
|
locale,
|
||||||
|
customDomain,
|
||||||
authenticityToken,
|
authenticityToken,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ interface ITenant {
|
|||||||
siteName: string;
|
siteName: string;
|
||||||
siteLogo: string;
|
siteLogo: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
customDomain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ITenant;
|
export default ITenant;
|
||||||
@@ -4,6 +4,7 @@ interface ITenantJSON {
|
|||||||
site_logo: string;
|
site_logo: string;
|
||||||
brand_display_setting: string;
|
brand_display_setting: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
custom_domain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ITenantJSON;
|
export default ITenantJSON;
|
||||||
@@ -27,9 +27,9 @@ class OAuth < ApplicationRecord
|
|||||||
# for this reason, we don't preprend tenant subdomain
|
# for this reason, we don't preprend tenant subdomain
|
||||||
# but rather use the "login" subdomain
|
# but rather use the "login" subdomain
|
||||||
if self.is_default?
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Tenant < ApplicationRecord
|
|||||||
validates :site_name, presence: true
|
validates :site_name, presence: true
|
||||||
validates :subdomain, presence: true, uniqueness: true
|
validates :subdomain, presence: true, uniqueness: true
|
||||||
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
|
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
|
accepts_nested_attributes_for :tenant_setting, update_only: true
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TenantPolicy < ApplicationPolicy
|
|||||||
|
|
||||||
def permitted_attributes_for_update
|
def permitted_attributes_for_update
|
||||||
if user.admin?
|
if user.admin?
|
||||||
[:site_name, :site_logo, :locale]
|
[:site_name, :site_logo, :locale, :custom_domain]
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link_to t('mailers.devise.confirmation_instructions.action'),
|
<%= 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>
|
</p>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link_to t('mailers.devise.reset_password.action'),
|
<%= 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>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link_to 'Unlock my account',
|
<%= 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>
|
</p>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="margin: 16px auto; text-align: center;">
|
<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) %>
|
<%= image_tag('https://raw.githubusercontent.com/astuto/astuto-assets/main/logo-64.png', alt: 'Astuto Logo', size: 64) %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
showVoteButtonInBoard: @tenant_setting.show_vote_button_in_board,
|
showVoteButtonInBoard: @tenant_setting.show_vote_button_in_board,
|
||||||
showPoweredBy: @tenant_setting.show_powered_by,
|
showPoweredBy: @tenant_setting.show_powered_by,
|
||||||
rootBoardId: @tenant_setting.root_board_id.to_s,
|
rootBoardId: @tenant_setting.root_board_id.to_s,
|
||||||
|
customDomain: @tenant.custom_domain,
|
||||||
showRoadmapInHeader: @tenant_setting.show_roadmap_in_header,
|
showRoadmapInHeader: @tenant_setting.show_roadmap_in_header,
|
||||||
collapseBoardsInHeader: @tenant_setting.collapse_boards_in_header,
|
collapseBoardsInHeader: @tenant_setting.collapse_boards_in_header,
|
||||||
locale: @tenant.locale
|
locale: @tenant.locale
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<%=
|
<%=
|
||||||
t(
|
t(
|
||||||
'mailers.user.unsubscribe_from_post_html',
|
'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>
|
</p>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<%=
|
<%=
|
||||||
t(
|
t(
|
||||||
'mailers.user.unsubscribe_from_site_html',
|
'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
|
).html_safe
|
||||||
%>
|
%>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
||||||
|
|
||||||
<p>
|
<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>
|
</p>
|
||||||
|
|
||||||
<%= render 'user_mailer/closing' %>
|
<%= render 'user_mailer/closing' %>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<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>
|
</p>
|
||||||
|
|
||||||
<%= render 'user_mailer/closing' %>
|
<%= render 'user_mailer/closing' %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
||||||
|
|
||||||
<p>
|
<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>
|
</p>
|
||||||
|
|
||||||
<%= render 'user_mailer/closing' %>
|
<%= render 'user_mailer/closing' %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
<%= render 'user_mailer/quoted_text', text: @comment.body %>
|
||||||
|
|
||||||
<p>
|
<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>
|
</p>
|
||||||
|
|
||||||
<%= render 'user_mailer/closing' %>
|
<%= 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 << ".localhost:3000"
|
||||||
config.hosts << ".lvh.me:3000" # used to test oauth strategies in development
|
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
|
# 0 if using localhost, 1 if using lvh.me
|
||||||
config.action_dispatch.tld_length = 0
|
config.action_dispatch.tld_length = 0
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ en:
|
|||||||
site_logo: 'Site logo'
|
site_logo: 'Site logo'
|
||||||
subdomain: 'Subdomain'
|
subdomain: 'Subdomain'
|
||||||
locale: 'Language'
|
locale: 'Language'
|
||||||
|
custom_domain: 'Custom domain'
|
||||||
tenant_setting:
|
tenant_setting:
|
||||||
brand_display: 'Display'
|
brand_display: 'Display'
|
||||||
show_vote_count: 'Show vote count to users'
|
show_vote_count: 'Show vote count to users'
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ en:
|
|||||||
collapse_boards_in_header_no_collapse: 'Never'
|
collapse_boards_in_header_no_collapse: 'Never'
|
||||||
collapse_boards_in_header_always_collapse: 'Always'
|
collapse_boards_in_header_always_collapse: 'Always'
|
||||||
subtitle_visibility: 'Visibility'
|
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_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.'
|
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:
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -153,10 +153,11 @@ ActiveRecord::Schema.define(version: 2024_03_03_103945) do
|
|||||||
t.string "site_logo"
|
t.string "site_logo"
|
||||||
t.string "subdomain", null: false
|
t.string "subdomain", null: false
|
||||||
t.string "locale", default: "en"
|
t.string "locale", default: "en"
|
||||||
t.string "custom_url"
|
t.string "custom_domain"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.integer "status"
|
t.integer "status"
|
||||||
|
t.index ["custom_domain"], name: "index_tenants_on_custom_domain", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ FactoryBot.define do
|
|||||||
site_logo { "" }
|
site_logo { "" }
|
||||||
sequence(:subdomain) { |n| "mysubdomain#{n}" }
|
sequence(:subdomain) { |n| "mysubdomain#{n}" }
|
||||||
locale { "en" }
|
locale { "en" }
|
||||||
custom_url { "" }
|
custom_domain { nil }
|
||||||
|
status { "active" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,4 +35,38 @@ RSpec.describe Tenant, type: :model do
|
|||||||
tenant.subdomain = tenant2.subdomain
|
tenant.subdomain = tenant2.subdomain
|
||||||
expect(tenant).to be_invalid
|
expect(tenant).to be_invalid
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user