From 0ad1b5eec08340370ffe9f5bc3409c1305e67781 Mon Sep 17 00:00:00 2001 From: Riccardo Graziosi <31478034+riggraz@users.noreply.github.com> Date: Thu, 29 Aug 2024 22:14:04 +0200 Subject: [PATCH] Add private feedback space setting (#392) --- .../components/SiteSettings/index.scss | 12 ++ app/controllers/application_controller.rb | 27 ++++ app/controllers/registrations_controller.rb | 49 ++++++ .../AuthenticationIndexPage.tsx | 148 ++++++++++++++---- .../AuthenticationSiteSettingsP.tsx | 14 +- .../Authentication/OAuthProvidersList.tsx | 11 ++ .../SiteSettings/Authentication/index.tsx | 3 + .../General/GeneralSiteSettingsP.tsx | 24 +++ .../components/common/SiteSettingsInfoBox.tsx | 17 +- .../containers/AuthenticationSiteSettings.tsx | 16 ++ .../containers/GeneralSiteSettings.tsx | 2 + app/javascript/interfaces/ITenantSetting.ts | 13 ++ app/models/tenant_setting.rb | 6 + app/policies/tenant_setting_policy.rb | 3 + app/views/devise/registrations/new.html.erb | 68 ++++---- .../site_settings/authentication.html.erb | 4 + app/views/site_settings/general.html.erb | 1 + config/locales/backend/backend.en.yml | 4 + config/locales/en.yml | 10 +- ...22349_add_is_private_to_tenant_settings.rb | 5 + ..._registration_policy_to_tenant_settings.rb | 6 + db/schema.rb | 5 +- spec/factories/tenant_settings.rb | 1 + spec/models/tenant_setting_spec.rb | 20 +++ .../site_settings_general_spec.rb | 4 +- 25 files changed, 404 insertions(+), 69 deletions(-) create mode 100644 db/migrate/20240821122349_add_is_private_to_tenant_settings.rb create mode 100644 db/migrate/20240821133530_add_email_registration_policy_to_tenant_settings.rb diff --git a/app/assets/stylesheets/components/SiteSettings/index.scss b/app/assets/stylesheets/components/SiteSettings/index.scss index 2c7ddad4..d00901b2 100644 --- a/app/assets/stylesheets/components/SiteSettings/index.scss +++ b/app/assets/stylesheets/components/SiteSettings/index.scss @@ -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; + } } \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09123f0e..55dc034e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index e3738349..6fdd12f3 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -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 diff --git a/app/javascript/components/SiteSettings/Authentication/AuthenticationIndexPage.tsx b/app/javascript/components/SiteSettings/Authentication/AuthenticationIndexPage.tsx index 66452566..393f9785 100644 --- a/app/javascript/components/SiteSettings/Authentication/AuthenticationIndexPage.tsx +++ b/app/javascript/components/SiteSettings/Authentication/AuthenticationIndexPage.tsx @@ -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; setPage: React.Dispatch>; setSelectedOAuth: React.Dispatch>; } const AuthenticationIndexPage = ({ + originForm, oAuths, isSubmitting, submitError, @@ -30,38 +43,115 @@ const AuthenticationIndexPage = ({ handleToggleEnabledOAuth, handleToggleEnabledDefaultOAuth, handleDeleteOAuth, + handleUpdateTenantSettings, setPage, setSelectedOAuth, -}: Props) => ( - <> - -

{ I18n.t('site_settings.authentication.title') }

+}: Props) => { + const { + register, + handleSubmit, + formState: { isDirty, isSubmitSuccessful, errors }, + setValue, + } = useForm({ + defaultValues: { + emailRegistrationPolicy: originForm.emailRegistrationPolicy, + allowedEmailDomains: originForm.allowedEmailDomains, + }, + }); -

- window.open('https://docs.astuto.io/category/oauth-configuration/', '_blank')} - icon={} - > - {I18n.t('site_settings.authentication.learn_more')} - -

+ const onSubmit: SubmitHandler = data => { + handleUpdateTenantSettings( + data.emailRegistrationPolicy as TenantSettingEmailRegistrationPolicy, + data.allowedEmailDomains, + ).then(res => { + if (res?.status !== HttpStatus.OK) return; - + +

{ I18n.t('site_settings.authentication.title') }

+ +
+

{ I18n.t('site_settings.authentication.email_registration_subtitle') }

+ +
+
+ + +
+ + { + originForm.emailRegistrationPolicy === TENANT_SETTING_EMAIL_REGISTRATION_POLICY_CUSTOM_DOMAINS_ALLOWED && + <> +
+ + +
+ { + e.stopPropagation(); + setIsDirtyAllowedEmails(e.target.value !== originForm.allowedEmailDomains); + }} + style={{marginRight: 8}} + id="allowedEmailDomains" + type="text" + className="formControl" + /> + +
+ + + { I18n.t('site_settings.authentication.allowed_email_domains_help') } + +
+ + } +
+
+ +
+ + +
+ + -
- - - -); + + ); +} export default AuthenticationIndexPage; \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Authentication/AuthenticationSiteSettingsP.tsx b/app/javascript/components/SiteSettings/Authentication/AuthenticationSiteSettingsP.tsx index b414b3d5..03419327 100644 --- a/app/javascript/components/SiteSettings/Authentication/AuthenticationSiteSettingsP.tsx +++ b/app/javascript/components/SiteSettings/Authentication/AuthenticationSiteSettingsP.tsx @@ -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; 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 => { + return onUpdateTenantSettings(emailRegistrationPolicy, allowedEmailDomains, authenticityToken); + } + return ( page === 'index' ? ; @@ -31,6 +33,15 @@ const OAuthProvidersList = ({ +

+ window.open('https://docs.astuto.io/category/oauth-configuration/', '_blank')} + icon={} + > + {I18n.t('site_settings.authentication.learn_more')} + +

+
    { oAuths.map((oAuth, i) => ( diff --git a/app/javascript/components/SiteSettings/Authentication/index.tsx b/app/javascript/components/SiteSettings/Authentication/index.tsx index 9522e1e7..d2f92cc8 100644 --- a/app/javascript/components/SiteSettings/Authentication/index.tsx +++ b/app/javascript/components/SiteSettings/Authentication/index.tsx @@ -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 { return ( diff --git a/app/javascript/components/SiteSettings/General/GeneralSiteSettingsP.tsx b/app/javascript/components/SiteSettings/General/GeneralSiteSettingsP.tsx index b8d04e6e..6149242a 100644 --- a/app/javascript/components/SiteSettings/General/GeneralSiteSettingsP.tsx +++ b/app/javascript/components/SiteSettings/General/GeneralSiteSettingsP.tsx @@ -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 = ({ } +
    +

    { I18n.t('site_settings.general.subtitle_privacy') }

    + +
    +
    + + + + { I18n.t('site_settings.general.is_private_help') } + +
    +
    +
    +

    { I18n.t('site_settings.general.subtitle_moderation') }

    @@ -361,6 +379,12 @@ const GeneralSiteSettingsP = ({ areUpdating={areUpdating} error={error} areDirty={isDirty && !isSubmitSuccessful} + isSticky={isDirty && !isSubmitSuccessful} + saveButton={ + + } /> ); diff --git a/app/javascript/components/common/SiteSettingsInfoBox.tsx b/app/javascript/components/common/SiteSettingsInfoBox.tsx index 78d17c94..64ca97d2 100644 --- a/app/javascript/components/common/SiteSettingsInfoBox.tsx +++ b/app/javascript/components/common/SiteSettingsInfoBox.tsx @@ -8,10 +8,18 @@ interface Props { areUpdating: boolean; error: string; areDirty?: boolean; + isSticky?: boolean; + saveButton?: React.ReactNode; } -const SiteSettingsInfoBox = ({ areUpdating, error, areDirty = false }: Props) => ( - +const SiteSettingsInfoBox = ({ + areUpdating, + error, + areDirty = false, + isSticky = false, + saveButton = null, +}: Props) => ( + { areUpdating ? @@ -22,7 +30,10 @@ const SiteSettingsInfoBox = ({ areUpdating, error, areDirty = false }: Props) => : areDirty ? - { I18n.t('site_settings.info_box.dirty') } + <> + { I18n.t('site_settings.info_box.dirty') } + {saveButton && saveButton} + : { I18n.t('site_settings.info_box.up_to_date') } } diff --git a/app/javascript/containers/AuthenticationSiteSettings.tsx b/app/javascript/containers/AuthenticationSiteSettings.tsx index 5973eacb..aa690225 100644 --- a/app/javascript/containers/AuthenticationSiteSettings.tsx +++ b/app/javascript/containers/AuthenticationSiteSettings.tsx @@ -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 { + return dispatch(updateTenant({ + tenantSetting: { + email_registration_policy: emailRegistrationPolicy, + allowed_email_domains: allowedEmailDomains, + }, + authenticityToken, + })); + }, }); export default connect( diff --git a/app/javascript/containers/GeneralSiteSettings.tsx b/app/javascript/containers/GeneralSiteSettings.tsx index d62db91a..10e56710 100644 --- a/app/javascript/containers/GeneralSiteSettings.tsx +++ b/app/javascript/containers/GeneralSiteSettings.tsx @@ -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, diff --git a/app/javascript/interfaces/ITenantSetting.ts b/app/javascript/interfaces/ITenantSetting.ts index b869841b..85fa9a14 100644 --- a/app/javascript/interfaces/ITenantSetting.ts +++ b/app/javascript/interfaces/ITenantSetting.ts @@ -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; diff --git a/app/models/tenant_setting.rb b/app/models/tenant_setting.rb index 59c9854e..b07ce253 100644 --- a/app/models/tenant_setting.rb +++ b/app/models/tenant_setting.rb @@ -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, diff --git a/app/policies/tenant_setting_policy.rb b/app/policies/tenant_setting_policy.rb index 034ba406..aa15259c 100644 --- a/app/policies/tenant_setting_policy.rb +++ b/app/policies/tenant_setting_policy.rb @@ -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, diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 0c82a66b..6d9641cc 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -4,42 +4,46 @@ <%= render "devise/shared/error_messages", resource: resource %> -
    - <%= f.label :full_name, class: "sr-only" %> - <%= f.text_field :full_name, - placeholder: t('common.forms.auth.full_name'), - required: true, - class: "form-control" %> -
    + <% unless Current.tenant.tenant_setting.email_registration_policy == "none_allowed" %> +
    + <%= f.label :full_name, class: "sr-only" %> + <%= f.text_field :full_name, + placeholder: t('common.forms.auth.full_name'), + required: true, + class: "form-control" %> +
    -
    - <%= f.label :email, class: "sr-only" %> - <%= f.email_field :email, - autocomplete: "email", - placeholder: t('common.forms.auth.email'), - required: true, - class: "form-control" %> -
    +
    + <%= f.label :email, class: "sr-only" %> + <%= f.email_field :email, + autocomplete: "email", + placeholder: t('common.forms.auth.email'), + required: true, + class: "form-control" %> +
    -
    - <%= f.label :password, class: "sr-only" %> - <%= f.password_field :password, - placeholder: t('common.forms.auth.password'), - required: true, - class: "form-control" %> -
    +
    + <%= f.label :password, class: "sr-only" %> + <%= f.password_field :password, + placeholder: t('common.forms.auth.password'), + required: true, + class: "form-control" %> +
    -
    - <%= f.label :password_confirmation, class: "sr-only" %> - <%= f.password_field :password_confirmation, - placeholder: t('common.forms.auth.password_confirmation'), - required: true, - class: "form-control" %> -
    +
    + <%= f.label :password_confirmation, class: "sr-only" %> + <%= f.password_field :password_confirmation, + placeholder: t('common.forms.auth.password_confirmation'), + required: true, + class: "form-control" %> +
    -
    - <%= f.submit t('common.forms.auth.sign_up'), class: "btnPrimary btn-block" %> -
    +
    + <%= f.submit t('common.forms.auth.sign_up'), class: "btnPrimary btn-block" %> +
    + <% else %> +

    <%= t('common.forms.auth.email_registration_not_allowed') %>

    + <% end %> <% end %> <%= render "devise/shared/o_auths", is_sign_up: true %> diff --git a/app/views/site_settings/authentication.html.erb b/app/views/site_settings/authentication.html.erb index 15a15e10..28ada15e 100644 --- a/app/views/site_settings/authentication.html.erb +++ b/app/views/site_settings/authentication.html.erb @@ -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 } ) diff --git a/app/views/site_settings/general.html.erb b/app/views/site_settings/general.html.erb index 04d0703c..70a12eaf 100644 --- a/app/views/site_settings/general.html.erb +++ b/app/views/site_settings/general.html.erb @@ -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, diff --git a/config/locales/backend/backend.en.yml b/config/locales/backend/backend.en.yml index 48c7cb5a..e9867ec3 100644 --- a/config/locales/backend/backend.en.yml +++ b/config/locales/backend/backend.en.yml @@ -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' diff --git a/config/locales/en.yml b/config/locales/en.yml index d1784545..5b7e255b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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' diff --git a/db/migrate/20240821122349_add_is_private_to_tenant_settings.rb b/db/migrate/20240821122349_add_is_private_to_tenant_settings.rb new file mode 100644 index 00000000..2b76fe94 --- /dev/null +++ b/db/migrate/20240821122349_add_is_private_to_tenant_settings.rb @@ -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 diff --git a/db/migrate/20240821133530_add_email_registration_policy_to_tenant_settings.rb b/db/migrate/20240821133530_add_email_registration_policy_to_tenant_settings.rb new file mode 100644 index 00000000..e1d61391 --- /dev/null +++ b/db/migrate/20240821133530_add_email_registration_policy_to_tenant_settings.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 41c625d1..11668828 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/factories/tenant_settings.rb b/spec/factories/tenant_settings.rb index af9329c5..575fce8a 100644 --- a/spec/factories/tenant_settings.rb +++ b/spec/factories/tenant_settings.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :tenant_setting do tenant + is_private { false } end end diff --git a/spec/models/tenant_setting_spec.rb b/spec/models/tenant_setting_spec.rb index 292a86db..8e8dd9f5 100644 --- a/spec/models/tenant_setting_spec.rb +++ b/spec/models/tenant_setting_spec.rb @@ -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 diff --git a/spec/system/site_settings/site_settings_general_spec.rb b/spec/system/site_settings/site_settings_general_spec.rb index 37a48f3a..bd8182ce 100644 --- a/spec/system/site_settings/site_settings_general_spec.rb +++ b/spec/system/site_settings/site_settings_general_spec.rb @@ -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')