From e317edecb85b5995696508aab9eef0e30cef062b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?= Date: Mon, 12 Feb 2024 22:05:32 +0900 Subject: [PATCH] =?UTF-8?q?Add:=20#348=20=E6=96=B0=E8=A6=8F=E7=99=BB?= =?UTF-8?q?=E9=8C=B2=E3=81=AE=E4=B8=8A=E9=99=90=E4=BA=BA=E6=95=B0=20(#527)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add: #348 新規登録の上限人数 * Fix test * Fix test * Wip * Fix test * Add invite support * Wip * Fix test * Fix test * Fix test --- .../settings/registrations_controller.rb | 9 + .../auth/confirmations_controller.rb | 6 + app/helpers/registration_helper.rb | 4 +- app/helpers/registration_limitation_helper.rb | 55 ++++++ .../closed_registrations_modal/index.jsx | 12 +- app/javascript/mastodon/initial_state.js | 2 + app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/locales/ja.json | 1 + app/models/form/admin_settings.rb | 12 ++ app/models/user.rb | 4 + app/serializers/initial_state_serializer.rb | 4 +- app/serializers/node_info/serializer.rb | 3 +- app/serializers/rest/instance_serializer.rb | 4 +- .../rest/v1/instance_serializer.rb | 3 +- app/services/delete_account_service.rb | 3 + .../settings/registrations/show.html.haml | 14 ++ .../confirmations/limitation_error.html.haml | 11 ++ config/locales/devise.en.yml | 1 + config/locales/devise.ja.yml | 1 + config/locales/en.yml | 3 + config/locales/ja.yml | 3 + config/locales/simple_form.en.yml | 6 + config/locales/simple_form.ja.yml | 10 ++ config/settings.yml | 6 + .../auth/registrations_controller_spec.rb | 158 ++++++++++++++++++ spec/models/user_spec.rb | 32 ++++ 26 files changed, 362 insertions(+), 6 deletions(-) create mode 100644 app/helpers/registration_limitation_helper.rb create mode 100644 app/views/auth/confirmations/limitation_error.html.haml diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb index b4a74349c0..6dbc86df9a 100644 --- a/app/controllers/admin/settings/registrations_controller.rb +++ b/app/controllers/admin/settings/registrations_controller.rb @@ -1,9 +1,18 @@ # frozen_string_literal: true class Admin::Settings::RegistrationsController < Admin::SettingsController + include RegistrationLimitationHelper + + before_action :set_limitation_counts, only: :show # rubocop:disable Rails/LexicallyScopedActionFilter + private def after_update_redirect_path admin_settings_registrations_path end + + def set_limitation_counts + @current_users_count = user_count_for_registration + @current_users_count_today = today_increase_user_count + end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 7ca7be5f8e..fdbde5d730 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -2,6 +2,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController include Auth::CaptchaConcern + include RegistrationLimitationHelper layout 'auth' @@ -16,6 +17,11 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController skip_before_action :require_functional! def show + if reach_registrations_limit? + render :limitation_error + return + end + old_session_values = session.to_hash reset_session session.update old_session_values.except('session_id') diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb index ef5462ac88..c3db46c027 100644 --- a/app/helpers/registration_helper.rb +++ b/app/helpers/registration_helper.rb @@ -3,8 +3,10 @@ module RegistrationHelper extend ActiveSupport::Concern + include RegistrationLimitationHelper + def allowed_registration?(remote_ip, invite) - !Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip) + !Rails.configuration.x.single_user_mode && !omniauth_only? && ((registrations_open? && !reach_registrations_limit?) || invite&.valid_for_use?) && !ip_blocked?(remote_ip) end def registrations_open? diff --git a/app/helpers/registration_limitation_helper.rb b/app/helpers/registration_limitation_helper.rb new file mode 100644 index 0000000000..65e295720c --- /dev/null +++ b/app/helpers/registration_limitation_helper.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module RegistrationLimitationHelper + def reach_registrations_limit? + return true unless registrations_in_time? + + ((Setting.registrations_limit.presence || 0).positive? && Setting.registrations_limit <= user_count_for_registration) || + ((Setting.registrations_limit_per_day.presence || 0).positive? && Setting.registrations_limit_per_day <= today_increase_user_count) + end + + def user_count_for_registration + Rails.cache.fetch('registrations:user_count') { User.confirmed.enabled.joins(:account).merge(Account.without_suspended).count } + end + + def today_increase_user_count + today_date = Time.now.utc.beginning_of_day.to_i + count = 0 + + if Rails.cache.fetch('registrations:today_date') { today_date } == today_date + count = Rails.cache.fetch('registrations:today_increase_user_count') { today_increase_user_count_value } + else + count = today_increase_user_count_value + Rails.cache.write('registrations:today_date', today_date) + Rails.cache.write('registrations:today_increase_user_count', count) + end + + count + end + + def today_increase_user_count_value + User.confirmed.enabled.where('users.created_at >= ?', Time.now.utc.beginning_of_day).joins(:account).merge(Account.without_suspended).count + end + + def registrations_in_time? + start_hour = Setting.registrations_start_hour || 0 + end_hour = Setting.registrations_end_hour || 24 + secondary_start_hour = Setting.registrations_secondary_start_hour || 0 + secondary_end_hour = Setting.registrations_secondary_end_hour || 0 + + return true if start_hour >= end_hour && secondary_start_hour >= secondary_end_hour + + current_hour = Time.now.utc.hour + primary_permitted = false + primary_permitted = start_hour <= current_hour && current_hour < end_hour if start_hour < end_hour && end_hour.positive? + secondary_permitted = false + secondary_permitted = secondary_start_hour <= current_hour && current_hour < secondary_end_hour if secondary_start_hour < secondary_end_hour && secondary_end_hour.positive? + + primary_permitted || secondary_permitted + end + + def reset_registration_limit_caches! + Rails.cache.delete('registrations:user_count') + Rails.cache.delete('registrations:today_increase_user_count') + end +end diff --git a/app/javascript/mastodon/features/closed_registrations_modal/index.jsx b/app/javascript/mastodon/features/closed_registrations_modal/index.jsx index 89ced8029c..a8565f4797 100644 --- a/app/javascript/mastodon/features/closed_registrations_modal/index.jsx +++ b/app/javascript/mastodon/features/closed_registrations_modal/index.jsx @@ -4,7 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import { fetchServer } from 'mastodon/actions/server'; -import { domain } from 'mastodon/initial_state'; +import { domain, registrationsReachLimit } from 'mastodon/initial_state'; const mapStateToProps = state => ({ message: state.getIn(['server', 'server', 'registrations', 'message']), @@ -27,6 +27,16 @@ class ClosedRegistrationsModal extends ImmutablePureComponent { dangerouslySetInnerHTML={{ __html: this.props.message }} /> ); + } else if (registrationsReachLimit) { + closedRegistrationsMessage = ( +
+
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 49b23789d2..b2c02ae5d8 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -48,6 +48,7 @@ * @property {string=} owner * @property {boolean} profile_directory * @property {boolean} registrations_open + * @property {boolean} registrations_reach_limit * @property {boolean} reduce_motion * @property {string} repository * @property {boolean} search_enabled @@ -134,6 +135,7 @@ export const owner = getMeta('owner'); export const profile_directory = getMeta('profile_directory'); export const reduceMotion = getMeta('reduce_motion'); export const registrationsOpen = getMeta('registrations_open'); +export const registrationsReachLimit = getMeta('registrations_reach_limit'); export const repository = getMeta('repository'); export const searchEnabled = getMeta('search_enabled'); export const trendsEnabled = getMeta('trends_enabled'); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 220bf87be9..4dd6fa9663 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -113,6 +113,7 @@ "circles.edit": "Edit circle", "closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.", "closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.", + "closed_registrations_modal.description_when_reaching_limit": "New registrations are currently temporarily restricted. Either the maximum number of registrations has been reached or it is outside the time frame available for registration. Please contact the administrator for more information or wait until the restriction is lifted.", "closed_registrations_modal.find_another_server": "Find another server", "closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!", "closed_registrations_modal.title": "Signing up on Mastodon", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 02ce8a0ead..59fc559491 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -170,6 +170,7 @@ "circles.subheading": "あなたのサークル", "closed_registrations.other_server_instructions": "Mastodonは分散型なので他のサーバーにアカウントを作ってもこのサーバーとやり取りできます。", "closed_registrations_modal.description": "現在{domain}でアカウント作成はできませんがMastodonは{domain}のアカウントでなくても利用できます。", + "closed_registrations_modal.description_when_reaching_limit": "新規登録は現在一時的に制限されています。登録の上限人数に達したか、または登録可能な時間帯の範囲外です。詳細を管理人に問い合わせるか、制限が解除されるまでお待ち下さい。", "closed_registrations_modal.find_another_server": "別のサーバーを探す", "closed_registrations_modal.preamble": "Mastodonは分散型なのでどのサーバーでアカウントを作成してもこのサーバーのユーザーを誰でもフォローして交流することができます。また自分でホスティングすることもできます!", "closed_registrations_modal.title": "Mastodonでアカウントを作成", diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 2231695b77..555e443316 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -15,6 +15,12 @@ class Form::AdminSettings registrations_mode closed_registrations_message registration_button_message + registrations_limit + registrations_limit_per_day + registrations_start_hour + registrations_end_hour + registrations_secondary_start_hour + registrations_secondary_end_hour timeline_preview bootstrap_timeline_accounts theme @@ -61,6 +67,12 @@ class Form::AdminSettings content_cache_retention_period backups_retention_period post_hash_tags_max + registrations_limit + registrations_limit_per_day + registrations_start_hour + registrations_end_hour + registrations_secondary_start_hour + registrations_secondary_end_hour ).freeze BOOLEAN_KEYS = %i( diff --git a/app/models/user.rb b/app/models/user.rb index f84654ef8d..a102b2cb94 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -55,6 +55,7 @@ class User < ApplicationRecord include LanguagesHelper include Redisable + include RegistrationLimitationHelper include User::HasSettings include User::LdapAuthenticable include User::Omniauthable @@ -192,6 +193,8 @@ class User < ApplicationRecord end def confirm + raise Mastodon::ValidationError, I18n.t('devise.registrations.sign_up_failed_because_reach_limit') if !invited? && reach_registrations_limit? + wrap_email_confirmation do super end @@ -482,6 +485,7 @@ class User < ApplicationRecord ActivityTracker.record('activity:logins', id) UserMailer.welcome(self).deliver_later TriggerWebhookWorker.perform_async('account.approved', 'Account', account_id) + reset_registration_limit_caches! end def prepare_returning_user! diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 9115085a64..c59d4235ba 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -3,6 +3,7 @@ class InitialStateSerializer < ActiveModel::Serializer include RoutingHelper include DtlHelper + include RegistrationLimitationHelper attributes :meta, :compose, :accounts, :media_attachments, :settings, @@ -122,7 +123,8 @@ class InitialStateSerializer < ActiveModel::Serializer locale: I18n.locale, mascot: instance_presenter.mascot&.file&.url, profile_directory: Setting.profile_directory, - registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode, + registrations_open: Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode, + registrations_reach_limit: Setting.registrations_mode != 'none' && reach_registrations_limit?, repository: Mastodon::Version.repository, search_enabled: Chewy.enabled?, single_user_mode: Rails.configuration.x.single_user_mode, diff --git a/app/serializers/node_info/serializer.rb b/app/serializers/node_info/serializer.rb index bdf1ca884d..cf52518ad0 100644 --- a/app/serializers/node_info/serializer.rb +++ b/app/serializers/node_info/serializer.rb @@ -3,6 +3,7 @@ class NodeInfo::Serializer < ActiveModel::Serializer include RoutingHelper include KmyblueCapabilitiesHelper + include RegistrationLimitationHelper attributes :version, :software, :protocols, :services, :usage, :open_registrations, :metadata @@ -35,7 +36,7 @@ class NodeInfo::Serializer < ActiveModel::Serializer end def open_registrations - Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode + Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode end def metadata diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 5eb3b7fe94..38010e4949 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -9,6 +9,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer include RoutingHelper include KmyblueCapabilitiesHelper + include RegistrationLimitationHelper attributes :domain, :title, :version, :source_url, :description, :usage, :thumbnail, :languages, :configuration, @@ -110,6 +111,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer { enabled: registrations_enabled?, approval_required: Setting.registrations_mode == 'approved', + limit_reached: Setting.registrations_mode != 'none' && reach_registrations_limit?, message: registrations_enabled? ? nil : registrations_message, url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil), } @@ -118,7 +120,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer private def registrations_enabled? - Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode + Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode end def registrations_message diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index de794df843..74ceec385b 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -3,6 +3,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer include RoutingHelper include KmyblueCapabilitiesHelper + include RegistrationLimitationHelper attributes :uri, :title, :short_description, :description, :email, :version, :urls, :stats, :thumbnail, @@ -109,7 +110,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer end def registrations - Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode + Setting.registrations_mode != 'none' && !reach_registrations_limit? && !Rails.configuration.x.single_user_mode end def approval_required diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 2bb7134f04..8bb6fdc960 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -2,6 +2,7 @@ class DeleteAccountService < BaseService include Payloadable + include RegistrationLimitationHelper ASSOCIATIONS_ON_SUSPEND = %w( account_notes @@ -143,6 +144,8 @@ class DeleteAccountService < BaseService else @account.user.destroy end + + reset_registration_limit_caches! end def purge_content! diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index 3674c6c2d3..dbf46c5cca 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -27,5 +27,19 @@ .fields-group = f.input :registration_button_message, as: :text, kmyblue: true, hint: false, wrapper: :with_label, input_html: { rows: 2 } + .fields-group + = f.input :registrations_limit, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }, label: I18n.t('simple_form.labels.form_admin_settings.registrations_limit', count: @current_users_count) + + .fields-group + = f.input :registrations_limit_per_day, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' }, label: I18n.t('simple_form.labels.form_admin_settings.registrations_limit_per_day', count: @current_users_count_today) + + .fields-group + = f.input :registrations_start_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' } + = f.input :registrations_end_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' } + + .fields-group + = f.input :registrations_secondary_start_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' } + = f.input :registrations_secondary_end_hour, kmyblue: true, wrapper: :with_label, input_html: { pattern: '[0-9]+' } + .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/auth/confirmations/limitation_error.html.haml b/app/views/auth/confirmations/limitation_error.html.haml new file mode 100644 index 0000000000..8fdc6ec2a1 --- /dev/null +++ b/app/views/auth/confirmations/limitation_error.html.haml @@ -0,0 +1,11 @@ +- content_for :page_title do + = t('auth.registration_limit.title') + += form_tag root_url, method: 'GET', class: 'simple_form' do + = render 'auth/shared/progress', stage: 'confirm' + + %h1.title= t('auth.registration_limit.title') + %p.lead= t('auth.registration_limit.hint_html') + + .actions + = button_tag t('challenge.confirm'), class: 'button', type: :submit diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 4439397c8e..cf2fc087fc 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -93,6 +93,7 @@ en: updated_not_active: Your password has been changed successfully. registrations: destroyed: Bye! Your account has been successfully cancelled. We hope to see you again soon. + sign_up_failed_because_reach_limit: You cannot create account because reaching limit. signed_up: Welcome! You have signed up successfully. signed_up_but_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated. signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked. diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml index 44a9a31839..47ee41a5ec 100644 --- a/config/locales/devise.ja.yml +++ b/config/locales/devise.ja.yml @@ -93,6 +93,7 @@ ja: updated_not_active: パスワードは正常に更新されました。 registrations: destroyed: アカウントの作成はキャンセルされました。またのご利用をお待ちしています。 + sign_up_failed_because_reach_limit: 制限に到達しているため、アカウントを作成できません。 signed_up: アカウントの作成が完了しました。Mastodonへようこそ。 signed_up_but_inactive: アカウントの作成が完了しました。しかし、アカウントが有効化されていないためログインできませんでした。 signed_up_but_locked: アカウントの作成が完了しました。しかし、アカウントがロックされているためログインできませんでした。 diff --git a/config/locales/en.yml b/config/locales/en.yml index 244c34e537..dde3e35fa0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1267,6 +1267,9 @@ en: saml: SAML register: Sign up registration_closed: "%{instance} is not accepting new members" + registration_limit: + hint_html: The maximum number of new registrations allowed, or the time of day, is restricted by the administrator. You will not be able to complete authentication while the restriction is in effect. Please contact the administrator or wait until the restriction is lifted. If you have reached the limit, you will need to register again. + title: New registrations are currently restricted resend_confirmation: Resend confirmation link reset_password: Reset password rules: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a80a2698dd..901bacac93 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1255,6 +1255,9 @@ ja: saml: SAML register: 登録する registration_closed: "%{instance}は現在、新規登録停止中です" + registration_limit: + hint_html: 新規登録可能な上限人数、もしくは時間帯は、管理者によって制限されています。制限中は認証を完了することができません。管理者に問い合わせるか、制限が解除されるまでお待ちください。なお認証期限に達した場合は、再度新規登録を行う必要があります。 + title: 新規登録は現在制限中です resend_confirmation: 確認メールを再送 reset_password: パスワードを再発行 rules: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7443f48777..b2e8990fd5 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -354,7 +354,13 @@ en: profile_directory: Enable profile directory receive_other_servers_emoji_reaction: Receive emoji reaction between other server users registration_button_message: Register button message + registrations_limit: User registration limit + registrations_limit_per_day: User registration limit per day registrations_mode: Who can sign-up + registrations_end_hour: Registration end hour (UTC 0-24) + registrations_start_hour: Registration start hour (UTC 0-24) + registrations_secondary_end_hour: Secondary registration end hour (UTC 0-24) If input 0, secondary hour is disabled. + registrations_secondary_start_hour: Secondary registration start hour (UTC 0-24) require_invite_text: Require a reason to join show_domain_blocks: Show domain blocks show_domain_blocks_rationale: Show why domains were blocked diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index bca4245c6e..8c0b90977d 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -117,6 +117,10 @@ ja: peers_api_enabled: このサーバーが Fediverse で遭遇したドメイン名のリストです。このサーバーが知っているだけで、特定のサーバーと連合しているかのデータは含まれません。これは一般的に Fediverse に関する統計情報を収集するサービスによって使用されます。 profile_directory: ディレクトリには、掲載する設定をしたすべてのユーザーが一覧表示されます。 receive_other_servers_emoji_reaction: 負荷の原因になります。人が少ない場合にのみ有効にすることをおすすめします。 + registrations_limit: 現在のユーザー数がこれを超過すると、管理者がこの数値を増やさない限り新規登録できません。0を指定すると、この制限を無効化します。 + registrations_limit_per_day: 本日登録されたユーザー数がこれを超過すると、UTC時刻で翌日0時にならない限り新規登録できません。0を指定すると、この制限を無効化します。 + registrations_end_hour: 新規登録可能な時間帯の開始時間を指定します。これより前の時間に登録することはできません。終了時間より後にすることはできません。 + registrations_start_hour: 新規登録可能な時間帯の終了時間を指定します。これより後の時間に登録することはできません。開始時間より前にすることはできません。 require_invite_text: アカウント登録が承認制の場合、登録の際の申請事由の入力を必須にします site_contact_email: 法律またはサポートに関する問い合わせ先 site_contact_username: マストドンでの連絡方法 @@ -363,7 +367,13 @@ ja: profile_directory: プロフィール一覧を有効にする receive_other_servers_emoji_reaction: 他のサーバーのユーザーが他のサーバーの投稿につけた絵文字リアクションを受け入れる registration_button_message: 新規登録ボタンの直上に表示するメッセージ + registrations_limit: 登録ユーザー数上限 (現在 %{count} 名) + registrations_limit_per_day: 1日あたり登録ユーザー数上限 (現在 %{count} 名) registrations_mode: 新規登録が可能な人 + registrations_end_hour: 登録受付終了時間 A (UTC 0〜24) + registrations_start_hour: 登録受付開始時間 A (UTC 0〜24) + registrations_secondary_end_hour: 登録受付終了時間 B (UTC 0〜24) ここで0を指定した場合、時間Bの設定は無効化されます + registrations_secondary_start_hour: 登録受付開始時間 B (UTC 0〜24) require_invite_text: 申請事由の入力を必須にする show_domain_blocks: ドメインブロックを表示 show_domain_blocks_rationale: ドメインがブロックされた理由を表示 diff --git a/config/settings.yml b/config/settings.yml index 62280dd60f..9ffa0a7d5b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -10,6 +10,12 @@ defaults: &defaults site_contact_username: '' site_contact_email: '' registrations_mode: 'open' + registrations_limit: 0 + registrations_limit_per_day: 0 + registrations_start_hour: 0 + registrations_end_hour: 24 + registrations_secondary_start_hour: 0 + registrations_secondary_end_hour: 0 profile_directory: true closed_registrations_message: '' timeline_preview: true diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index 0b7f02f590..605f8c109b 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -280,6 +280,164 @@ RSpec.describe Auth::RegistrationsController do end end + context 'when max user count is set' do + subject do + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } } + end + + let(:users_max) { 3 } + + before do + Fabricate(:user) + Fabricate(:user) + Setting.registrations_mode = 'open' + Setting.registrations_limit = users_max + request.headers['Accept-Language'] = accept_language + end + + it 'redirects to setup' do + subject + expect(response).to redirect_to auth_setup_path + end + + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + end + + context 'when limit is reached' do + let(:users_max) { 2 } + + it 'does not create user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to be_nil + end + + context 'with invite' do + subject do + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', invite_code: invite.code, agreement: 'true' } } + end + + let(:invite) { Fabricate(:invite) } + + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + end + end + end + end + + context 'when max user count per day is set' do + subject do + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } } + end + + let(:users_max) { 2 } + let(:created_at) { Time.now.utc } + let(:precreate_users) { true } + + before do + Fabricate(:user, created_at: created_at) if precreate_users + Setting.registrations_mode = 'open' + Setting.registrations_limit_per_day = users_max + request.headers['Accept-Language'] = accept_language + end + + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + end + + context 'when limit is reached' do + let(:users_max) { 2 } + let(:created_at) { Time.now.utc - 1.day } + let(:precreate_users) { false } + + before do + travel_to Time.now.utc - 1.day + Fabricate(:user) + create_other_user + end + + it 'does not create user yesterday' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to be_nil + end + + it 'creates user' do + travel_to Time.now.utc + 1.day + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + end + end + + def create_other_user + post :create, params: { user: { account_attributes: { username: 'ohagi' }, email: 'test@ohagi.com', password: 'ohagi_must_be_tsubuan', password_confirmation: 'ohagi_must_be_tsubuan', agreement: 'true' } } + end + end + + context 'when registration time range is set' do + subject do + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } } + end + + shared_examples 'registration with time' do |header, start_hour_val, end_hour_val, secondary_start_hour_val, secondary_end_hour_val, result| # rubocop:disable Metrics/ParameterLists + context header do + let(:start_hour) { start_hour_val } + let(:end_hour) { end_hour_val } + let(:secondary_start_hour) { secondary_start_hour_val } + let(:secondary_end_hour) { secondary_end_hour_val } + + before do + Setting.registrations_mode = 'open' + Setting.registrations_start_hour = start_hour + Setting.registrations_end_hour = end_hour + Setting.registrations_secondary_start_hour = secondary_start_hour + Setting.registrations_secondary_end_hour = secondary_end_hour + request.headers['Accept-Language'] = accept_language + + travel_to Time.now.utc.beginning_of_day + 10.hours + end + + if result + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + end + else + it 'does not create user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to be_nil + end + end + end + end + + it_behaves_like 'registration with time', 'time range is not set', 0, 24, 0, 0, true + it_behaves_like 'registration with time', 'time range is set', 9, 12, 0, 0, true + it_behaves_like 'registration with time', 'time range is out of range', 12, 15, 0, 0, false + it_behaves_like 'registration with time', 'time range is invalid', 20, 15, 0, 0, true + it_behaves_like 'registration with time', 'secondary time range is set', 0, 4, 9, 12, true + it_behaves_like 'registration with time', 'secondary time range is out of range', 0, 4, 12, 15, false + it_behaves_like 'registration with time', 'secondary time range is invalid', 0, 4, 20, 15, false + it_behaves_like 'registration with time', 'both time range are invalid', 4, 0, 20, 15, true + it_behaves_like 'registration with time', 'only secondary time range is set', 0, 0, 9, 12, true + end + include_examples 'checks for enabled registrations', :create end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5ac41c0ff1..2c5a69bf2a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -233,6 +233,38 @@ RSpec.describe User do expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) end end + + context 'when max user count is set' do + let(:users_max) { 3 } + + before do + Fabricate(:user) + Fabricate(:user) + Setting.registrations_limit = users_max + end + + it 'creates user' do + expect { subject }.to_not raise_error + expect(user.confirmed?).to be true + end + + context 'with limit is reached' do + let(:users_max) { 2 } + + it 'does not create user' do + expect { subject }.to raise_error Mastodon::ValidationError + expect(user.confirmed?).to be false + end + + it 'but creates user when invited' do + invite = Fabricate(:invite, user: Fabricate(:user), max_uses: nil, expires_at: 1.hour.from_now) + user.update!(invite: invite) + + expect { subject }.to_not raise_error + expect(user.confirmed?).to be true + end + end + end end end