Add onboarding state selector for self-hosted signup (#251)

* Add onboarding modes to self-hosted signup

* Style form consistently

* Configure ONBOARDING_STATE via ENV
This commit is contained in:
Juan José Mata
2025-10-27 21:52:37 +01:00
committed by GitHub
parent dcb674835c
commit 72e7d7736b
16 changed files with 121 additions and 23 deletions

View File

@@ -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

View File

@@ -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 =

View File

@@ -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
DISABLE_PARALLELIZATION=false

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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" } } %>
<div class="form-field w-fit">
<%= 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" } } %>
</div>
<% end %>
</div>
@@ -27,7 +39,7 @@
<% end %>
</div>
<% if Setting.require_invite_for_signup %>
<% if Setting.onboarding_state == "invite_only" %>
<div class="flex items-center justify-between mb-4">
<div>
<span class="text-primary text-base font-medium"><%= t(".generated_tokens") %></span>

View File

@@ -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.

View File

@@ -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
password_placeholder: Angi passordet ditt

View File

@@ -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
password_placeholder: Şifrenizi girin

View File

@@ -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

View File

@@ -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
not_authorized: Du er ikke autorisert til å utføre denne handlingen

View File

@@ -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: ı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
not_authorized: Bu işlemi gerçekleştirmek için yetkiniz yok

View File

@@ -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" } }

View File

@@ -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)