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)