mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 11:17:49 +01:00
Add private feedback space setting (#392)
This commit is contained in:
committed by
GitHub
parent
2d7f454d0a
commit
0ad1b5eec0
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
FactoryBot.define do
|
||||
factory :tenant_setting do
|
||||
tenant
|
||||
is_private { false }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user