diff --git a/.env.example b/.env.example index 23b85ed72..0a0d75aec 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ # Enables self hosting features (should be set to true unless you know what you're doing) SELF_HOSTED=true +# Controls onboarding flow (valid: open, closed, invite_only) +ONBOARDING_STATE=open + # Secret key used to encrypt credentials (https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base) # Has to be a random string, generated eg. by running `openssl rand -hex 64` SECRET_KEY_BASE=secret-value diff --git a/.env.local.example b/.env.local.example index 830a73906..7aacba4a2 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,6 +1,9 @@ # To enable / disable self-hosting features. SELF_HOSTED = true +# Controls onboarding flow (valid: open, closed, invite_only) +ONBOARDING_STATE = open + # Enable Twelve market data (careful, this will use your API credits) TWELVE_DATA_API_KEY = diff --git a/.env.test.example b/.env.test.example index 4c6c62cda..57d67d064 100644 --- a/.env.test.example +++ b/.env.test.example @@ -1,5 +1,8 @@ SELF_HOSTED=false +# Controls onboarding flow (valid: open, closed, invite_only) +ONBOARDING_STATE=open + # OpenID Connect for tests OIDC_ISSUER= OIDC_CLIENT_ID= @@ -21,4 +24,4 @@ OIDC_REDIRECT_URI=http://localhost:3000/auth/openid_connect/callback COVERAGE=false # Set to true to run test suite serially -DISABLE_PARALLELIZATION=false \ No newline at end of file +DISABLE_PARALLELIZATION=false diff --git a/app/controllers/concerns/invitable.rb b/app/controllers/concerns/invitable.rb index 5e12d2df9..a295e859f 100644 --- a/app/controllers/concerns/invitable.rb +++ b/app/controllers/concerns/invitable.rb @@ -8,7 +8,11 @@ module Invitable private def invite_code_required? return false if @invitation.present? - self_hosted? ? Setting.require_invite_for_signup : ENV["REQUIRE_INVITE_CODE"] == "true" + if self_hosted? + Setting.onboarding_state == "invite_only" + else + ENV["REQUIRE_INVITE_CODE"] == "true" + end end def self_hosted? diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b57b508d9..83287e708 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -3,6 +3,7 @@ class RegistrationsController < ApplicationController layout "auth" + before_action :ensure_signup_open, if: :self_hosted? before_action :set_user, only: :create before_action :set_invitation before_action :claim_invite_code, only: :create, if: :invite_code_required? @@ -79,4 +80,10 @@ class RegistrationsController < ApplicationController render :new, status: :unprocessable_entity end end + + def ensure_signup_open + return unless Setting.onboarding_state == "closed" + + redirect_to new_session_path, alert: t("registrations.closed") + end end diff --git a/app/controllers/settings/hostings_controller.rb b/app/controllers/settings/hostings_controller.rb index c86a6dac0..048a1c463 100644 --- a/app/controllers/settings/hostings_controller.rb +++ b/app/controllers/settings/hostings_controller.rb @@ -15,8 +15,9 @@ class Settings::HostingsController < ApplicationController end def update - if hosting_params.key?(:require_invite_for_signup) - Setting.require_invite_for_signup = hosting_params[:require_invite_for_signup] + if hosting_params.key?(:onboarding_state) + onboarding_state = hosting_params[:onboarding_state].to_s + Setting.onboarding_state = onboarding_state end if hosting_params.key?(:require_email_confirmation) @@ -68,7 +69,7 @@ class Settings::HostingsController < ApplicationController private def hosting_params - params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :brand_fetch_client_id, :twelve_data_api_key, :openai_access_token, :openai_uri_base, :openai_model) + params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :brand_fetch_client_id, :twelve_data_api_key, :openai_access_token, :openai_uri_base, :openai_model) end def ensure_admin diff --git a/app/models/setting.rb b/app/models/setting.rb index fbbe65256..afffd70f2 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -10,9 +10,40 @@ class Setting < RailsSettings::Base field :openai_model, type: :string, default: ENV["OPENAI_MODEL"] field :brand_fetch_client_id, type: :string, default: ENV["BRAND_FETCH_CLIENT_ID"] + ONBOARDING_STATES = %w[open closed invite_only].freeze + DEFAULT_ONBOARDING_STATE = begin + env_value = ENV["ONBOARDING_STATE"].to_s.presence || "open" + ONBOARDING_STATES.include?(env_value) ? env_value : "open" + end + + field :onboarding_state, type: :string, default: DEFAULT_ONBOARDING_STATE field :require_invite_for_signup, type: :boolean, default: false field :require_email_confirmation, type: :boolean, default: ENV.fetch("REQUIRE_EMAIL_CONFIRMATION", "true") == "true" + def self.validate_onboarding_state!(state) + return if ONBOARDING_STATES.include?(state) + + raise ValidationError, I18n.t("settings.hostings.update.invalid_onboarding_state") + end + + class << self + alias_method :raw_onboarding_state, :onboarding_state + alias_method :raw_onboarding_state=, :onboarding_state= + + def onboarding_state + value = raw_onboarding_state + return "invite_only" if value.blank? && require_invite_for_signup + + value.presence || DEFAULT_ONBOARDING_STATE + end + + def onboarding_state=(state) + validate_onboarding_state!(state) + self.require_invite_for_signup = state == "invite_only" + self.raw_onboarding_state = state + end + end + # Validates OpenAI configuration requires model when custom URI base is set def self.validate_openai_config!(uri_base: nil, model: nil) # Use provided values or current settings diff --git a/app/views/settings/hostings/_invite_code_settings.html.erb b/app/views/settings/hostings/_invite_code_settings.html.erb index 3a89f214d..14e4439e3 100644 --- a/app/views/settings/hostings/_invite_code_settings.html.erb +++ b/app/views/settings/hostings/_invite_code_settings.html.erb @@ -9,7 +9,19 @@ url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "change" } do |form| %> - <%= form.toggle :require_invite_for_signup, { data: { auto_submit_form_target: "auto" } } %> +
+ <%= form.select :onboarding_state, + options_for_select( + [ + [ t(".states.open"), "open" ], + [ t(".states.closed"), "closed" ], + [ t(".states.invite_only"), "invite_only" ] + ], + Setting.onboarding_state + ), + { label: false }, + { data: { auto_submit_form_target: "auto" } } %> +
<% end %> @@ -27,7 +39,7 @@ <% end %> - <% if Setting.require_invite_for_signup %> + <% if Setting.onboarding_state == "invite_only" %>
<%= t(".generated_tokens") %> diff --git a/config/locales/views/registrations/en.yml b/config/locales/views/registrations/en.yml index 4c02397cf..86b1fb192 100644 --- a/config/locales/views/registrations/en.yml +++ b/config/locales/views/registrations/en.yml @@ -8,6 +8,7 @@ en: user: create: Continue registrations: + closed: Signups are currently closed. create: failure: There was a problem signing up. invalid_invite_code: Invalid invite code, please try again. diff --git a/config/locales/views/registrations/nb.yml b/config/locales/views/registrations/nb.yml index 1c462c075..a915358cf 100644 --- a/config/locales/views/registrations/nb.yml +++ b/config/locales/views/registrations/nb.yml @@ -8,6 +8,7 @@ nb: user: create: Fortsett registrations: + closed: Registrering er midlertidig stengt. create: failure: Det oppsto et problem med registreringen. invalid_invite_code: Ugyldig invitasjonskode, vennligst prøv igjen. @@ -22,4 +23,4 @@ nb: welcome_body: For å komme i gang må du registrere deg for en ny konto. Du vil da kunne konfigurere flere innstillinger i appen. welcome_title: Velkommen til Self Hosted %{product_name}! - password_placeholder: Angi passordet ditt \ No newline at end of file + password_placeholder: Angi passordet ditt diff --git a/config/locales/views/registrations/tr.yml b/config/locales/views/registrations/tr.yml index e22b57e0c..dbc067a0e 100644 --- a/config/locales/views/registrations/tr.yml +++ b/config/locales/views/registrations/tr.yml @@ -8,6 +8,7 @@ tr: user: create: Devam Et registrations: + closed: Kayıtlar şu anda kapalı. create: failure: Kayıt olurken bir sorun oluştu. invalid_invite_code: Geçersiz davet kodu, lütfen tekrar deneyin. @@ -21,4 +22,4 @@ tr: title: Hesabınızı oluşturun welcome_body: Başlamak için yeni bir hesap oluşturmalısınız. Daha sonra uygulama içinde ek ayarları yapılandırabileceksiniz. welcome_title: Self Hosted Maybe'ye Hoş Geldiniz! - password_placeholder: Şifrenizi girin \ No newline at end of file + password_placeholder: Şifrenizi girin diff --git a/config/locales/views/settings/hostings/en.yml b/config/locales/views/settings/hostings/en.yml index 5be096d85..e769dfdcf 100644 --- a/config/locales/views/settings/hostings/en.yml +++ b/config/locales/views/settings/hostings/en.yml @@ -3,17 +3,20 @@ en: settings: hostings: invite_code_settings: - description: Every new user that joins your instance of %{product} can only do - so via an invite code + description: Control how new people sign up for your instance of %{product}. email_confirmation_description: When enabled, users must confirm their email address when changing it. email_confirmation_title: Require email confirmation generate_tokens: Generate new code generated_tokens: Generated codes - title: Require invite code for signup + title: Onboarding + states: + open: Open + closed: Closed + invite_only: Invite-only show: general: External Services - invites: Invite Codes + invites: Onboarding title: Self-Hosting danger_zone: Danger Zone clear_cache: Clear data cache @@ -47,6 +50,7 @@ en: update: failure: Invalid setting value success: Settings updated + invalid_onboarding_state: Invalid onboarding state clear_cache: cache_cleared: Data cache has been cleared. This may take a few moments to complete. not_authorized: You are not authorized to perform this action diff --git a/config/locales/views/settings/hostings/nb.yml b/config/locales/views/settings/hostings/nb.yml index 2b59cd0f8..923b89d29 100644 --- a/config/locales/views/settings/hostings/nb.yml +++ b/config/locales/views/settings/hostings/nb.yml @@ -3,16 +3,20 @@ nb: settings: hostings: invite_code_settings: - description: Hver ny bruker som blir med i din instans av %{product_name} kan bare bli med via en invitasjonskode + description: Kontroller hvordan nye personer registrerer seg for din instans av %{product}. email_confirmation_description: Når aktivert, må brukere bekrefte e-postadressen sin når de endrer den. email_confirmation_title: Krev e-postbekreftelse generate_tokens: Generer ny kode generated_tokens: Genererte koder - title: Krev invitasjonskode for registrering + title: Onboarding + states: + open: Åpen + closed: Stengt + invite_only: Kun invitasjon show: general: Generelle innstillinger - invites: Invitasjonskoder + invites: Onboarding title: Selvhosting danger_zone: Fareområde clear_cache: Tøm cache @@ -23,6 +27,7 @@ nb: update: failure: Ugyldig innstillingsverdi success: Innstillinger oppdatert + invalid_onboarding_state: Ugyldig onboarding-modus clear_cache: cache_cleared: Cachen er tømt. Dette kan ta noen øyeblikk å fullføre. - not_authorized: Du er ikke autorisert til å utføre denne handlingen \ No newline at end of file + not_authorized: Du er ikke autorisert til å utføre denne handlingen diff --git a/config/locales/views/settings/hostings/tr.yml b/config/locales/views/settings/hostings/tr.yml index df03b00c1..551fdc784 100644 --- a/config/locales/views/settings/hostings/tr.yml +++ b/config/locales/views/settings/hostings/tr.yml @@ -3,15 +3,19 @@ tr: settings: hostings: invite_code_settings: - description: Maybe uygulamanıza katılan her yeni kullanıcı yalnızca bir davet kodu ile katılabilir + description: Yeni kullanıcıların %{product} örneğinize nasıl kaydolacağını kontrol edin. email_confirmation_description: Etkinleştirildiğinde, kullanıcılar e-posta adreslerini değiştirirken e-posta onayı yapmak zorundadır. email_confirmation_title: E-posta onayı gerektir generate_tokens: Yeni kod oluştur generated_tokens: Oluşturulan kodlar - title: Kayıt için davet kodu gerektir + title: Onboarding + states: + open: Açık + closed: Kapalı + invite_only: Davet ile show: general: Genel Ayarlar - invites: Davet Kodları + invites: Onboarding title: Kendi Sunucunda Barındırma danger_zone: Tehlikeli Bölge clear_cache: Veri önbelleğini temizle @@ -29,6 +33,7 @@ tr: update: failure: Geçersiz ayar değeri success: Ayarlar güncellendi + invalid_onboarding_state: Geçersiz onboarding durumu clear_cache: cache_cleared: Veri önbelleği temizlendi. Bu işlemin tamamlanması birkaç dakika sürebilir. - not_authorized: Bu işlemi gerçekleştirmek için yetkiniz yok \ No newline at end of file + not_authorized: Bu işlemi gerçekleştirmek için yetkiniz yok diff --git a/test/controllers/settings/hostings_controller_test.rb b/test/controllers/settings/hostings_controller_test.rb index ecc380ffe..a3c4be0ba 100644 --- a/test/controllers/settings/hostings_controller_test.rb +++ b/test/controllers/settings/hostings_controller_test.rb @@ -26,7 +26,7 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest get settings_hosting_url assert_response :forbidden - patch settings_hosting_url, params: { setting: { require_invite_for_signup: true } } + patch settings_hosting_url, params: { setting: { onboarding_state: "invite_only" } } assert_response :forbidden end end @@ -48,6 +48,20 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest end end + test "can update onboarding state when self hosting is enabled" do + with_self_hosting do + patch settings_hosting_url, params: { setting: { onboarding_state: "invite_only" } } + + assert_equal "invite_only", Setting.onboarding_state + assert Setting.require_invite_for_signup + + patch settings_hosting_url, params: { setting: { onboarding_state: "closed" } } + + assert_equal "closed", Setting.onboarding_state + refute Setting.require_invite_for_signup + end + end + test "can update openai access token when self hosting is enabled" do with_self_hosting do patch settings_hosting_url, params: { setting: { openai_access_token: "token" } } diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb index 0271f129d..deec32671 100644 --- a/test/system/settings_test.rb +++ b/test/system/settings_test.rb @@ -44,7 +44,10 @@ class SettingsTest < ApplicationSystemTestCase click_link "Self-Hosting" assert_current_path settings_hosting_path assert_selector "h1", text: "Self-Hosting" - check "setting[require_invite_for_signup]", allow_label_click: true + find("select#setting_onboarding_state").select("Invite-only") + within("select#setting_onboarding_state") do + assert_selector "option[selected]", text: "Invite-only" + end click_button "Generate new code" assert_selector 'span[data-clipboard-target="source"]', visible: true, count: 1 # invite code copy widget copy_button = find('button[data-action="clipboard#copy"]', match: :first) # Find the first copy button (adjust if needed)