Add private feedback space setting (#392)

This commit is contained in:
Riccardo Graziosi
2024-08-29 22:14:04 +02:00
committed by GitHub
parent 2d7f454d0a
commit 0ad1b5eec0
25 changed files with 404 additions and 69 deletions

View File

@@ -8,4 +8,16 @@
.warning {
color: #fd7e14;
}
}
.siteSettingsInfo.siteSettingsInfoSticky {
position: sticky;
bottom: 16px;
z-index: 100;
width: 80%;
margin: 0 auto;
span.warning {
margin-bottom: 8px;
}
}

View File

@@ -7,6 +7,8 @@ class ApplicationController < ActionController::Base
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :check_tenant_is_private, if: :should_check_tenant_is_private?
prepend_before_action :load_tenant_data
# Override Devise after sign in path
@@ -18,6 +20,15 @@ class ApplicationController < ActionController::Base
end
end
# Override Devise after sign out path
def after_sign_out_path_for(resource_or_scope)
if Current.tenant.tenant_setting.is_private
new_user_session_path
else
super
end
end
protected
def configure_permitted_parameters
@@ -69,6 +80,14 @@ class ApplicationController < ActionController::Base
}, status: :forbidden
end
def check_tenant_is_private
return unless Current.tenant.tenant_setting.is_private
return if user_signed_in?
flash[:alert] = t('errors.not_logged_in')
redirect_to new_user_session_path
end
private
def user_not_authorized
@@ -78,4 +97,12 @@ class ApplicationController < ActionController::Base
error: t('errors.unauthorized')
}, status: :unauthorized
end
def should_check_tenant_is_private?
controller_name == 'posts' ||
controller_name == 'boards' ||
controller_name == 'comments' ||
(controller_name == 'static_pages' && action_name == 'root') ||
(controller_name == 'static_pages' && action_name == 'roadmap')
end
end

View File

@@ -5,6 +5,32 @@ class RegistrationsController < Devise::RegistrationsController
before_action :set_page_title_new, only: [:new]
before_action :set_page_title_edit, only: [:edit]
# Override create to check whether email registration is possible
def create
ts = Current.tenant.tenant_setting
email = sign_up_params[:email]
if ts.email_registration_policy == "none_allowed" || (ts.email_registration_policy == "custom_domains_allowed" && !allowed_domain?(email))
flash[:alert] = t('errors.email_domain_not_allowed')
redirect_to new_user_registration_path and return
end
super
end
# Override update to check whether provided email is allowed
def update
ts = Current.tenant.tenant_setting
email = account_update_params[:email]
if ts.email_registration_policy == "custom_domains_allowed" && !allowed_domain?(email)
flash[:alert] = t('errors.email_domain_not_allowed')
redirect_to edit_user_registration_path and return
end
super
end
# Override destroy to soft delete
def destroy
resource.status = "deleted"
@@ -27,9 +53,32 @@ class RegistrationsController < Devise::RegistrationsController
render json: { success: true } # always return true, even if user not found
end
protected
def after_inactive_sign_up_path_for(resource)
if Current.tenant.tenant_setting.is_private
# Redirect to log in page, since root page only visible to logged in users
new_user_session_path
else
super
end
end
private
def allowed_domain?(email)
allowed_email_domains = Current.tenant.tenant_setting.allowed_email_domains
return false unless email.count('@') == 1
return true if allowed_email_domains.blank?
registering_domain = email.split('@').last
allowed_domains = allowed_email_domains.split(',')
allowed_domains.include?(registering_domain)
end
def set_page_title_new
@page_title = t('common.forms.auth.sign_up')
end

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import I18n from 'i18n-js';
import Box from '../../common/Box';
@@ -6,10 +7,20 @@ import OAuthProvidersList from './OAuthProvidersList';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import { OAuthsState } from '../../../reducers/oAuthsReducer';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import ActionLink from '../../common/ActionLink';
import { LearnMoreIcon } from '../../common/Icons';
import { TENANT_SETTING_EMAIL_REGISTRATION_POLICY_ALL_ALLOWED, TENANT_SETTING_EMAIL_REGISTRATION_POLICY_CUSTOM_DOMAINS_ALLOWED, TENANT_SETTING_EMAIL_REGISTRATION_POLICY_NONE_ALLOWED, TenantSettingEmailRegistrationPolicy } from '../../../interfaces/ITenantSetting';
import { getLabel } from '../../../helpers/formUtils';
import HttpStatus from '../../../constants/http_status';
import { SmallMutedText } from '../../common/CustomTexts';
import Button from '../../common/Button';
export interface IAuthenticationForm {
emailRegistrationPolicy: string;
allowedEmailDomains: string;
}
interface Props {
originForm: IAuthenticationForm;
oAuths: OAuthsState;
isSubmitting: boolean;
submitError: string;
@@ -17,12 +28,14 @@ interface Props {
handleToggleEnabledOAuth(id: number, enabled: boolean): void;
handleToggleEnabledDefaultOAuth(id: number, enabled: boolean): void;
handleDeleteOAuth(id: number): void;
handleUpdateTenantSettings(emailRegistrationPolicy: TenantSettingEmailRegistrationPolicy, allowedEmailDomains: string): Promise<any>;
setPage: React.Dispatch<React.SetStateAction<AuthenticationPages>>;
setSelectedOAuth: React.Dispatch<React.SetStateAction<number>>;
}
const AuthenticationIndexPage = ({
originForm,
oAuths,
isSubmitting,
submitError,
@@ -30,38 +43,115 @@ const AuthenticationIndexPage = ({
handleToggleEnabledOAuth,
handleToggleEnabledDefaultOAuth,
handleDeleteOAuth,
handleUpdateTenantSettings,
setPage,
setSelectedOAuth,
}: Props) => (
<>
<Box customClass="authenticationIndexPage">
<h2>{ I18n.t('site_settings.authentication.title') }</h2>
}: Props) => {
const {
register,
handleSubmit,
formState: { isDirty, isSubmitSuccessful, errors },
setValue,
} = useForm<IAuthenticationForm>({
defaultValues: {
emailRegistrationPolicy: originForm.emailRegistrationPolicy,
allowedEmailDomains: originForm.allowedEmailDomains,
},
});
<p style={{textAlign: 'left'}}>
<ActionLink
onClick={() => window.open('https://docs.astuto.io/category/oauth-configuration/', '_blank')}
icon={<LearnMoreIcon />}
>
{I18n.t('site_settings.authentication.learn_more')}
</ActionLink>
</p>
const onSubmit: SubmitHandler<IAuthenticationForm> = data => {
handleUpdateTenantSettings(
data.emailRegistrationPolicy as TenantSettingEmailRegistrationPolicy,
data.allowedEmailDomains,
).then(res => {
if (res?.status !== HttpStatus.OK) return;
<OAuthProvidersList
oAuths={oAuths.items}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}
window.location.reload();
});
};
const [isDirtyAllowedEmails, setIsDirtyAllowedEmails] = React.useState(false);
return (
<>
<Box customClass="authenticationIndexPage">
<h2>{ I18n.t('site_settings.authentication.title') }</h2>
<div className="emailRegistrationPolicy">
<h3>{ I18n.t('site_settings.authentication.email_registration_subtitle') }</h3>
<form onSubmit={handleSubmit(onSubmit)} onChange={handleSubmit(onSubmit)}>
<div className="formGroup">
<label htmlFor="brandSetting">{ getLabel('tenant_setting', 'email_registration_policy') }</label>
<select
{...register('emailRegistrationPolicy')}
id="emailRegistrationPolicy"
className="selectPicker"
>
<option value={TENANT_SETTING_EMAIL_REGISTRATION_POLICY_ALL_ALLOWED}>
{ I18n.t('site_settings.authentication.email_registration_policy_all') }
</option>
<option value={TENANT_SETTING_EMAIL_REGISTRATION_POLICY_NONE_ALLOWED}>
{ I18n.t('site_settings.authentication.email_registration_policy_none') }
</option>
<option value={TENANT_SETTING_EMAIL_REGISTRATION_POLICY_CUSTOM_DOMAINS_ALLOWED}>
{ I18n.t('site_settings.authentication.email_registration_policy_custom') }
</option>
</select>
</div>
{
originForm.emailRegistrationPolicy === TENANT_SETTING_EMAIL_REGISTRATION_POLICY_CUSTOM_DOMAINS_ALLOWED &&
<>
<div className="formGroup">
<label htmlFor="allowedEmailDomains">{ getLabel('tenant_setting', 'allowed_email_domains') }</label>
<div style={{display: 'flex'}}>
<input
{...register('allowedEmailDomains')}
onChange={(e) => {
e.stopPropagation();
setIsDirtyAllowedEmails(e.target.value !== originForm.allowedEmailDomains);
}}
style={{marginRight: 8}}
id="allowedEmailDomains"
type="text"
className="formControl"
/>
<Button onClick={() => null} disabled={!isDirtyAllowedEmails}>
{I18n.t('common.buttons.update')}
</Button>
</div>
<SmallMutedText>
{ I18n.t('site_settings.authentication.allowed_email_domains_help') }
</SmallMutedText>
</div>
</>
}
</form>
</div>
<br />
<OAuthProvidersList
oAuths={oAuths.items}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
handleDeleteOAuth={handleDeleteOAuth}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}
/>
</Box>
<SiteSettingsInfoBox
areUpdating={oAuths.areLoading || isSubmitting}
error={oAuths.error || submitError}
areDirty={isDirtyAllowedEmails}
/>
</Box>
<SiteSettingsInfoBox
areUpdating={oAuths.areLoading || isSubmitting}
error={oAuths.error || submitError}
/>
</>
);
</>
);
}
export default AuthenticationIndexPage;

View File

@@ -6,10 +6,13 @@ import { IOAuth } from '../../../interfaces/IOAuth';
import { OAuthsState } from '../../../reducers/oAuthsReducer';
import AuthenticationFormPage from './AuthenticationFormPage';
import AuthenticationIndexPage from './AuthenticationIndexPage';
import AuthenticationIndexPage, { IAuthenticationForm } from './AuthenticationIndexPage';
import { ISiteSettingsOAuthForm } from './OAuthForm';
import { TenantSettingEmailRegistrationPolicy } from '../../../interfaces/ITenantSetting';
interface Props {
originForm: IAuthenticationForm;
oAuths: OAuthsState;
requestOAuths(): void;
@@ -18,6 +21,7 @@ interface Props {
onToggleEnabledOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
onToggleEnabledDefaultOAuth(id: number, isEnabled: boolean, authenticityToken: string): void;
onDeleteOAuth(id: number, authenticityToken: string): void;
onUpdateTenantSettings(emailRegistrationPolicy: TenantSettingEmailRegistrationPolicy, allowedEmailDomains: string, authenticityToken: string): Promise<any>;
isSubmitting: boolean;
submitError: string;
@@ -28,6 +32,7 @@ interface Props {
export type AuthenticationPages = 'index' | 'new' | 'edit';
const AuthenticationSiteSettingsP = ({
originForm,
oAuths,
requestOAuths,
onSubmitOAuth,
@@ -35,6 +40,7 @@ const AuthenticationSiteSettingsP = ({
onToggleEnabledOAuth,
onToggleEnabledDefaultOAuth,
onDeleteOAuth,
onUpdateTenantSettings,
isSubmitting,
submitError,
authenticityToken,
@@ -68,13 +74,19 @@ const AuthenticationSiteSettingsP = ({
onDeleteOAuth(id, authenticityToken);
};
const handleUpdateTenantSettings = (emailRegistrationPolicy: TenantSettingEmailRegistrationPolicy, allowedEmailDomains: string): Promise<any> => {
return onUpdateTenantSettings(emailRegistrationPolicy, allowedEmailDomains, authenticityToken);
}
return (
page === 'index' ?
<AuthenticationIndexPage
originForm={originForm}
oAuths={oAuths}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}
handleToggleEnabledDefaultOAuth={handleToggleEnabledDefaultOAuth}
handleDeleteOAuth={handleDeleteOAuth}
handleUpdateTenantSettings={handleUpdateTenantSettings}
setPage={setPage}
setSelectedOAuth={setSelectedOAuth}
isSubmitting={isSubmitting}

View File

@@ -5,6 +5,8 @@ import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import Button from '../../common/Button';
import { IOAuth } from '../../../interfaces/IOAuth';
import OAuthProviderItem from './OAuthProviderItem';
import ActionLink from '../../common/ActionLink';
import { LearnMoreIcon } from '../../common/Icons';
interface Props {
oAuths: Array<IOAuth>;
@@ -31,6 +33,15 @@ const OAuthProvidersList = ({
</Button>
</div>
<p style={{textAlign: 'left'}}>
<ActionLink
onClick={() => window.open('https://docs.astuto.io/category/oauth-configuration/', '_blank')}
icon={<LearnMoreIcon />}
>
{I18n.t('site_settings.authentication.learn_more')}
</ActionLink>
</p>
<ul className="oAuthsList">
{
oAuths.map((oAuth, i) => (

View File

@@ -5,8 +5,10 @@ import { Store } from 'redux';
import AuthenticationSiteSettings from '../../../containers/AuthenticationSiteSettings';
import createStoreHelper from '../../../helpers/createStore';
import { State } from '../../../reducers/rootReducer';
import { IAuthenticationForm } from './AuthenticationIndexPage';
interface Props {
originForm: IAuthenticationForm;
authenticityToken: string;
}
@@ -23,6 +25,7 @@ class AuthenticationSiteSettingsRoot extends React.Component<Props> {
return (
<Provider store={this.store}>
<AuthenticationSiteSettings
originForm={this.props.originForm}
authenticityToken={this.props.authenticityToken}
/>
</Provider>

View File

@@ -27,6 +27,7 @@ export interface ISiteSettingsGeneralForm {
locale: string;
rootBoardId?: string;
customDomain?: string;
isPrivate: boolean;
allowAnonymousFeedback: boolean;
feedbackApprovalPolicy: string;
showRoadmapInHeader: boolean;
@@ -52,6 +53,7 @@ interface Props {
locale: string,
rootBoardId: number,
customDomain: string,
isPrivate: boolean,
allowAnonymousFeedback: boolean,
feedbackApprovalPolicy: string,
showRoadmapInHeader: boolean,
@@ -86,6 +88,7 @@ const GeneralSiteSettingsP = ({
locale: originForm.locale,
rootBoardId: originForm.rootBoardId,
customDomain: originForm.customDomain,
isPrivate: originForm.isPrivate,
allowAnonymousFeedback: originForm.allowAnonymousFeedback,
feedbackApprovalPolicy: originForm.feedbackApprovalPolicy,
showRoadmapInHeader: originForm.showRoadmapInHeader,
@@ -104,6 +107,7 @@ const GeneralSiteSettingsP = ({
data.locale,
Number(data.rootBoardId),
data.customDomain,
data.isPrivate,
data.allowAnonymousFeedback,
data.feedbackApprovalPolicy,
data.showRoadmapInHeader,
@@ -254,6 +258,20 @@ const GeneralSiteSettingsP = ({
</div>
}
<div id="privacy" className="settingsGroup">
<h4>{ I18n.t('site_settings.general.subtitle_privacy') }</h4>
<div className="formGroup">
<div className="checkboxSwitch">
<input {...register('isPrivate')} type="checkbox" id="is_private_checkbox" />
<label htmlFor="is_private_checkbox">{ getLabel('tenant_setting', 'is_private') }</label>
<SmallMutedText>
{ I18n.t('site_settings.general.is_private_help') }
</SmallMutedText>
</div>
</div>
</div>
<div id="moderation" className="settingsGroup">
<h4>{ I18n.t('site_settings.general.subtitle_moderation') }</h4>
@@ -361,6 +379,12 @@ const GeneralSiteSettingsP = ({
areUpdating={areUpdating}
error={error}
areDirty={isDirty && !isSubmitSuccessful}
isSticky={isDirty && !isSubmitSuccessful}
saveButton={
<Button onClick={handleSubmit(onSubmit)} disabled={!isDirty}>
{I18n.t('common.buttons.update')}
</Button>
}
/>
</>
);

View File

@@ -8,10 +8,18 @@ interface Props {
areUpdating: boolean;
error: string;
areDirty?: boolean;
isSticky?: boolean;
saveButton?: React.ReactNode;
}
const SiteSettingsInfoBox = ({ areUpdating, error, areDirty = false }: Props) => (
<Box customClass="siteSettingsInfo">
const SiteSettingsInfoBox = ({
areUpdating,
error,
areDirty = false,
isSticky = false,
saveButton = null,
}: Props) => (
<Box customClass={`siteSettingsInfo${isSticky ? ' siteSettingsInfoSticky' : ''}`}>
{
areUpdating ?
<Spinner />
@@ -22,7 +30,10 @@ const SiteSettingsInfoBox = ({ areUpdating, error, areDirty = false }: Props) =>
</span>
:
areDirty ?
<span className="warning">{ I18n.t('site_settings.info_box.dirty') }</span>
<>
<span className="warning">{ I18n.t('site_settings.info_box.dirty') }</span>
{saveButton && saveButton}
</>
:
<span>{ I18n.t('site_settings.info_box.up_to_date') }</span>
}

View File

@@ -9,6 +9,8 @@ import { ISiteSettingsOAuthForm } from "../components/SiteSettings/Authenticatio
import { IOAuth } from "../interfaces/IOAuth";
import { State } from "../reducers/rootReducer";
import { updateDefaultOAuth } from "../actions/OAuth/updateDefaultOAuth";
import { TenantSettingEmailRegistrationPolicy } from "../interfaces/ITenantSetting";
import { updateTenant } from "../actions/Tenant/updateTenant";
const mapStateToProps = (state: State) => ({
oAuths: state.oAuths,
@@ -41,6 +43,20 @@ const mapDispatchToProps = (dispatch: any) => ({
onDeleteOAuth(id: number, authenticityToken: string) {
dispatch(deleteOAuth(id, authenticityToken));
},
onUpdateTenantSettings(
emailRegistrationPolicy: TenantSettingEmailRegistrationPolicy,
allowedEmailDomains: string,
authenticityToken: string,
): Promise<any> {
return dispatch(updateTenant({
tenantSetting: {
email_registration_policy: emailRegistrationPolicy,
allowed_email_domains: allowedEmailDomains,
},
authenticityToken,
}));
},
});
export default connect(

View File

@@ -18,6 +18,7 @@ const mapDispatchToProps = (dispatch: any) => ({
locale: string,
rootBoardId: number,
customDomain: string,
isPrivate: boolean,
allowAnonymousFeedback: boolean,
feedbackApprovalPolicy: TenantSettingFeedbackApprovalPolicy,
showRoadmapInHeader: boolean,
@@ -33,6 +34,7 @@ const mapDispatchToProps = (dispatch: any) => ({
tenantSetting: {
brand_display: brandDisplaySetting,
root_board_id: rootBoardId,
is_private: isPrivate,
allow_anonymous_feedback: allowAnonymousFeedback,
feedback_approval_policy: feedbackApprovalPolicy,
show_roadmap_in_header: showRoadmapInHeader,

View File

@@ -10,6 +10,16 @@ export type TenantSettingBrandDisplay =
typeof TENANT_SETTING_BRAND_DISPLAY_LOGO_ONLY |
typeof TENANT_SETTING_BRAND_DISPLAY_NONE;
// Email registration policy
export const TENANT_SETTING_EMAIL_REGISTRATION_POLICY_ALL_ALLOWED = 'all_allowed';
export const TENANT_SETTING_EMAIL_REGISTRATION_POLICY_NONE_ALLOWED = 'none_allowed';
export const TENANT_SETTING_EMAIL_REGISTRATION_POLICY_CUSTOM_DOMAINS_ALLOWED = 'custom_domains_allowed';
export type TenantSettingEmailRegistrationPolicy =
typeof TENANT_SETTING_EMAIL_REGISTRATION_POLICY_ALL_ALLOWED |
typeof TENANT_SETTING_EMAIL_REGISTRATION_POLICY_NONE_ALLOWED |
typeof TENANT_SETTING_EMAIL_REGISTRATION_POLICY_CUSTOM_DOMAINS_ALLOWED;
// Feedback approval policy
export const TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_ANONYMOUS_REQUIRE_APPROVAL = 'anonymous_require_approval';
export const TENANT_SETTING_FEEDBACK_APPROVAL_POLICY_NEVER_REQUIRE_APPROVAL = 'never_require_approval';
@@ -32,6 +42,9 @@ export type TenantSettingCollapseBoardsInHeader =
interface ITenantSetting {
brand_display?: TenantSettingBrandDisplay;
root_board_id?: number;
is_private?: boolean;
email_registration_policy?: TenantSettingEmailRegistrationPolicy;
allowed_email_domains?: string;
allow_anonymous_feedback?: boolean;
feedback_approval_policy?: TenantSettingFeedbackApprovalPolicy;
show_vote_count?: boolean;

View File

@@ -17,6 +17,12 @@ class TenantSetting < ApplicationRecord
:always_collapse
]
enum email_registration_policy: [
:all_allowed,
:none_allowed,
:custom_domains_allowed
]
enum feedback_approval_policy: [
:anonymous_require_approval,
:never_require_approval,

View File

@@ -4,6 +4,9 @@ class TenantSettingPolicy < ApplicationPolicy
[
:brand_display,
:root_board_id,
:is_private,
:email_registration_policy,
:allowed_email_domains,
:allow_anonymous_feedback,
:feedback_approval_policy,
:show_vote_count,

View File

@@ -4,42 +4,46 @@
<%= render "devise/shared/error_messages", resource: resource %>
<div class="form-group">
<%= f.label :full_name, class: "sr-only" %>
<%= f.text_field :full_name,
placeholder: t('common.forms.auth.full_name'),
required: true,
class: "form-control" %>
</div>
<% unless Current.tenant.tenant_setting.email_registration_policy == "none_allowed" %>
<div class="form-group">
<%= f.label :full_name, class: "sr-only" %>
<%= f.text_field :full_name,
placeholder: t('common.forms.auth.full_name'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :email, class: "sr-only" %>
<%= f.email_field :email,
autocomplete: "email",
placeholder: t('common.forms.auth.email'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :email, class: "sr-only" %>
<%= f.email_field :email,
autocomplete: "email",
placeholder: t('common.forms.auth.email'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password, class: "sr-only" %>
<%= f.password_field :password,
placeholder: t('common.forms.auth.password'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password, class: "sr-only" %>
<%= f.password_field :password,
placeholder: t('common.forms.auth.password'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, class: "sr-only" %>
<%= f.password_field :password_confirmation,
placeholder: t('common.forms.auth.password_confirmation'),
required: true,
class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation, class: "sr-only" %>
<%= f.password_field :password_confirmation,
placeholder: t('common.forms.auth.password_confirmation'),
required: true,
class: "form-control" %>
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.sign_up'), class: "btnPrimary btn-block" %>
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.sign_up'), class: "btnPrimary btn-block" %>
</div>
<% else %>
<p><%= t('common.forms.auth.email_registration_not_allowed') %></p>
<% end %>
<% end %>
<%= render "devise/shared/o_auths", is_sign_up: true %>

View File

@@ -5,6 +5,10 @@
react_component(
'SiteSettings/Authentication',
{
originForm: {
emailRegistrationPolicy: @tenant_setting.email_registration_policy,
allowedEmailDomains: @tenant_setting.allowed_email_domains,
},
authenticityToken: form_authenticity_token
}
)

View File

@@ -14,6 +14,7 @@
showPoweredBy: @tenant_setting.show_powered_by,
rootBoardId: @tenant_setting.root_board_id.to_s,
customDomain: @tenant.custom_domain,
isPrivate: @tenant_setting.is_private,
allowAnonymousFeedback: @tenant_setting.allow_anonymous_feedback,
feedbackApprovalPolicy: @tenant_setting.feedback_approval_policy,
showRoadmapInHeader: @tenant_setting.show_roadmap_in_header,

View File

@@ -2,6 +2,7 @@ en:
errors:
unknown: 'An unknown error occurred'
unauthorized: 'You are not authorized'
email_domain_not_allowed: 'You cannot register with the provided email address'
not_logged_in: 'You must be logged in to access this page'
not_enough_privileges: 'You do not have the privilegies to access this page'
user_blocked_or_deleted: 'You cannot access your account because it has been blocked or deleted.'
@@ -124,6 +125,9 @@ en:
custom_domain: 'Custom domain'
tenant_setting:
brand_display: 'Display'
is_private: 'Private site'
email_registration_policy: 'Email registration policy'
allowed_email_domains: 'Allowed email domains'
allow_anonymous_feedback: 'Allow anonymous feedback'
feedback_approval_policy: 'Feedback approval policy'
show_vote_count: 'Show vote count to users'

View File

@@ -42,6 +42,7 @@ en:
resend_unlock_instructions: 'Resend unlock instructions'
change_password: 'Change password'
password_help: '%{count} characters minimum'
email_registration_not_allowed: 'Email registration not allowed'
comments_number:
one: '1 comment'
other: '%{count} comments'
@@ -181,8 +182,10 @@ en:
brand_setting_name: 'Name only'
brand_setting_logo: 'Logo only'
brand_setting_none: 'None'
subtitle_privacy: 'Privacy'
is_private_help: 'If you enable this setting, only logged in users will be able to see the content of the feedback space.'
subtitle_moderation: 'Moderation'
allow_anonymous_feedback_help: 'Unregistered users will be able to submit feedback.'
allow_anonymous_feedback_help: 'If you enable this setting, unregistered users will be able to submit feedback.'
feedback_approval_policy_anonymous_require_approval: 'Require approval for anonymous feedback only'
feedback_approval_policy_never_require_approval: 'Never require approval'
feedback_approval_policy_always_require_approval: 'Always require approval'
@@ -219,6 +222,11 @@ en:
authentication:
title: 'Authentication'
learn_more: 'Learn how to configure custom OAuth providers'
email_registration_subtitle: 'Email registration'
email_registration_policy_all: 'Anyone can register with email'
email_registration_policy_none: 'No one can register with email'
email_registration_policy_custom: 'Only users with certain email domains can register with email'
allowed_email_domains_help: 'Separate domains with commas. Example: "gmail.com,yahoo.com,hotmail.com". Leave blank to allow any domain.'
oauth_subtitle: 'OAuth providers'
default_oauth: 'Default OAuth provider'
copy_url: 'Copy URL'

View File

@@ -0,0 +1,5 @@
class AddIsPrivateToTenantSettings < ActiveRecord::Migration[6.1]
def change
add_column :tenant_settings, :is_private, :boolean, default: false, null: false
end
end

View File

@@ -0,0 +1,6 @@
class AddEmailRegistrationPolicyToTenantSettings < ActiveRecord::Migration[6.1]
def change
add_column :tenant_settings, :email_registration_policy, :integer, default: 0, null: false
add_column :tenant_settings, :allowed_email_domains, :string
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2024_07_08_191938) do
ActiveRecord::Schema.define(version: 2024_08_21_133530) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -168,6 +168,9 @@ ActiveRecord::Schema.define(version: 2024_07_08_191938) do
t.boolean "show_powered_by", default: true, null: false
t.boolean "allow_anonymous_feedback", default: true, null: false
t.integer "feedback_approval_policy", default: 0, null: false
t.boolean "is_private", default: false, null: false
t.integer "email_registration_policy", default: 0, null: false
t.string "allowed_email_domains"
t.index ["tenant_id"], name: "index_tenant_settings_on_tenant_id"
end

View File

@@ -1,5 +1,6 @@
FactoryBot.define do
factory :tenant_setting do
tenant
is_private { false }
end
end

View File

@@ -60,4 +60,24 @@ RSpec.describe TenantSetting, type: :model do
tenant_setting.feedback_approval_policy = 'always_require_approval'
expect(tenant_setting).to be_valid
end
it 'has a setting for making the site private' do
expect(tenant_setting.is_private).to be_falsey
tenant_setting.is_private = true
expect(tenant_setting).to be_valid
tenant_setting.is_private = false
expect(tenant_setting).to be_valid
end
it 'has a setting for email registration policy' do
expect(tenant_setting.email_registration_policy).to eq('all_allowed')
tenant_setting.email_registration_policy = 'none_allowed'
expect(tenant_setting).to be_valid
tenant_setting.email_registration_policy = 'custom_domains_allowed'
expect(tenant_setting).to be_valid
end
end

View File

@@ -23,7 +23,7 @@ feature 'site settings: general', type: :system, js: true do
fill_in 'Site name', with: new_site_name
fill_in 'Site logo', with: new_site_logo
click_button 'Save'
find('button', text: 'Save', match: :first).click
within '.siteSettingsInfo' do
expect(page).to have_content('All changes saved')
@@ -43,7 +43,7 @@ feature 'site settings: general', type: :system, js: true do
expect(Current.tenant.locale).not_to eq(new_site_language)
select_by_value 'locale', new_site_language
click_button 'Save'
find('button', text: 'Save', match: :first).click
within '.siteSettingsInfo' do
expect(page).to have_content('Tutte le modifiche sono state salvate')