diff --git a/app/controllers/invite_codes_controller.rb b/app/controllers/invite_codes_controller.rb new file mode 100644 index 00000000..0413f2ce --- /dev/null +++ b/app/controllers/invite_codes_controller.rb @@ -0,0 +1,10 @@ +class InviteCodesController < ApplicationController + def index + @invite_codes = InviteCode.all + end + + def create + InviteCode.generate! + redirect_back_or_to invite_codes_path, notice: "Code generated" + end +end diff --git a/app/controllers/settings/hostings_controller.rb b/app/controllers/settings/hostings_controller.rb index 3058074f..dbf5177a 100644 --- a/app/controllers/settings/hostings_controller.rb +++ b/app/controllers/settings/hostings_controller.rb @@ -55,13 +55,13 @@ class Settings::HostingsController < SettingsController end def hosting_params - permitted_params = params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password) + permitted_params = params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password, :require_invite_for_signup) result = {} result[:upgrades_mode] = permitted_params[:upgrades_mode] == "manual" ? "manual" : "auto" if permitted_params.key?(:upgrades_mode) result[:render_deploy_hook] = permitted_params[:render_deploy_hook] if permitted_params.key?(:render_deploy_hook) result[:upgrades_target] = permitted_params[:upgrades_mode] unless permitted_params[:upgrades_mode] == "manual" if permitted_params.key?(:upgrades_mode) - result.merge!(permitted_params.slice(:email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password)) + result.merge!(permitted_params.slice(:email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password, :require_invite_for_signup)) result end diff --git a/app/javascript/controllers/clipboard_controller.js b/app/javascript/controllers/clipboard_controller.js new file mode 100644 index 00000000..34e29a28 --- /dev/null +++ b/app/javascript/controllers/clipboard_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + + +export default class extends Controller { + static targets = ["source", "iconDefault", "iconSuccess"] + + copy(event) { + event.preventDefault(); + if (this.sourceTarget && this.sourceTarget.textContent) { + navigator.clipboard.writeText(this.sourceTarget.textContent) + .then(() => { + this.showSuccess(); + }) + .catch((error) => { + console.error('Failed to copy text: ', error); + }); + } + } + + showSuccess() { + this.iconDefaultTarget.classList.add('hidden'); + this.iconSuccessTarget.classList.remove('hidden'); + setTimeout(() => { + this.iconDefaultTarget.classList.remove('hidden'); + this.iconSuccessTarget.classList.add('hidden'); + }, 3000); + } +} diff --git a/app/models/setting.rb b/app/models/setting.rb index 9ecadd37..2d177b31 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -22,6 +22,8 @@ class Setting < RailsSettings::Base field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"] + field :require_invite_for_signup, type: :boolean, default: false + scope :smtp_settings do field :smtp_host, type: :string, read_only: true, default: ENV["SMTP_ADDRESS"] field :smtp_port, type: :string, read_only: true, default: ENV["SMTP_PORT"] diff --git a/app/views/invite_codes/_invite_code.html.erb b/app/views/invite_codes/_invite_code.html.erb new file mode 100644 index 00000000..6eb1953e --- /dev/null +++ b/app/views/invite_codes/_invite_code.html.erb @@ -0,0 +1,16 @@ +<%# app/views/invite_codes/_invite_code.html.erb %> +
+
+
+ <%= invite_code.token %> +
+ +
+
diff --git a/app/views/invite_codes/index.html.erb b/app/views/invite_codes/index.html.erb new file mode 100644 index 00000000..d4d881d9 --- /dev/null +++ b/app/views/invite_codes/index.html.erb @@ -0,0 +1,12 @@ +<%# app/views/invite_codes/index.html.erb %> +<%= turbo_frame_tag "invite_codes" do %> + <% if @invite_codes.present? %> + <%= render @invite_codes %> + <% else %> +
+ <%= lucide_icon "binary", class: "w-6 h-6 text-sm text-gray-500" %> +

<%= t(".no_invite_codes") %>

+

<%= t(".invite_code_description") %>

+
+ <% end %> +<% end %> diff --git a/app/views/settings/hostings/show.html.erb b/app/views/settings/hostings/show.html.erb index b785a757..a96e68cb 100644 --- a/app/views/settings/hostings/show.html.erb +++ b/app/views/settings/hostings/show.html.erb @@ -2,46 +2,45 @@ <%= render "settings/nav" %> <% end %> -
+

<%= t(".page_title") %>

- <%= settings_section title: t(".general_settings_title") do %> - <%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, local: true, class: "space-y-6", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %> - <% if ENV["HOSTING_PLATFORM"] == "render" %> -
-

<%= t(".upgrades.title") %>

-

<%= t(".upgrades.description") %>

-
-
- <%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %> - <%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %> - <%= t(".upgrades.manual.title") %> -
- - <%= t(".upgrades.manual.description") %> - - <% end %> -
-
- <%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %> - <%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %> - <%= t(".upgrades.latest_release.title") %> -
- - <%= t(".upgrades.latest_release.description") %> - - <% end %> -
-
- <%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %> - <%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %> - <%= t(".upgrades.latest_commit.title") %> -
- - <%= t(".upgrades.latest_commit.description") %> - - <% end %> -
+ <% if ENV["HOSTING_PLATFORM"] == "render" %> + <%= settings_section title: t(".general_settings_title") do %> + <%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %> +

<%= t(".upgrades.title") %>

+

<%= t(".upgrades.description") %>

+ +
+
+ <%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %> + <%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %> + <%= t(".upgrades.manual.title") %> +
+ + <%= t(".upgrades.manual.description") %> + + <% end %> +
+
+ <%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %> + <%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %> + <%= t(".upgrades.latest_release.title") %> +
+ + <%= t(".upgrades.latest_release.description") %> + + <% end %> +
+
+ <%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %> + <%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %> + <%= t(".upgrades.latest_commit.title") %> +
+ + <%= t(".upgrades.latest_commit.description") %> + + <% end %>
@@ -51,37 +50,75 @@ <%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
<% end %> + <% end %> + <% end %> -
-

<%= t(".smtp_settings.title") %>

-

<%= t(".smtp_settings.description") %>

-
-
- <%= form.text_field :email_sender, label: t(".email_sender"), placeholder: t(".email_sender_placeholder"), value: Setting.email_sender, data: { "auto-submit-form-target" => "auto" } %> - <%= form.text_field :app_domain, label: t(".domain"), placeholder: t(".domain_placeholder"), value: Setting.app_domain, data: { "auto-submit-form-target" => "auto" } %> - <%= form.text_field :smtp_host, label: t(".smtp_settings.host"), placeholder: t(".smtp_settings.host_placeholder"), value: Setting.smtp_host, data: { "auto-submit-form-target" => "auto" } %> - <%= form.number_field :smtp_port, label: t(".smtp_settings.port"), placeholder: t(".smtp_settings.port_placeholder"), value: Setting.smtp_port, data: { "auto-submit-form-target" => "auto" } %> - <%= form.text_field :smtp_username, label: t(".smtp_settings.username"), placeholder: t(".smtp_settings.username_placeholder"), value: Setting.smtp_username, data: { "auto-submit-form-target" => "auto" } %> - <%= form.password_field :smtp_password, label: t(".smtp_settings.password"), placeholder: t(".smtp_settings.password_placeholder"), value: Setting.smtp_password, data: { "auto-submit-form-target" => "auto" } %> -
-
-
-
- <%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %> -
-
-

<%= t(".smtp_settings.send_test_email") %>

-

<%= t(".smtp_settings.send_test_email_description") %>

-
+ <%= settings_section title: t(".smtp_settings.title") do %> + <%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %> +

<%= t(".smtp_settings.description") %>

+
+
+ <%= form.text_field :email_sender, label: t(".email_sender"), placeholder: t(".email_sender_placeholder"), value: Setting.email_sender, data: { "auto-submit-form-target" => "auto" } %> + <%= form.text_field :app_domain, label: t(".domain"), placeholder: t(".domain_placeholder"), value: Setting.app_domain, data: { "auto-submit-form-target" => "auto" } %> + <%= form.text_field :smtp_host, label: t(".smtp_settings.host"), placeholder: t(".smtp_settings.host_placeholder"), value: Setting.smtp_host, data: { "auto-submit-form-target" => "auto" } %> + <%= form.number_field :smtp_port, label: t(".smtp_settings.port"), placeholder: t(".smtp_settings.port_placeholder"), value: Setting.smtp_port, data: { "auto-submit-form-target" => "auto" } %> + <%= form.text_field :smtp_username, label: t(".smtp_settings.username"), placeholder: t(".smtp_settings.username_placeholder"), value: Setting.smtp_username, data: { "auto-submit-form-target" => "auto" } %> + <%= form.password_field :smtp_password, label: t(".smtp_settings.password"), placeholder: t(".smtp_settings.password_placeholder"), value: Setting.smtp_password, data: { "auto-submit-form-target" => "auto" } %> +
+
+
+
+ <%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %>
- <%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class: "bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %> +

<%= t(".smtp_settings.send_test_email") %>

+

<%= t(".smtp_settings.send_test_email_description") %>

+
+ <%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class: "bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %> +
<% end %> <% end %> + <%= settings_section title: t(".invite_settings.title") do %> +
+
+
+

<%= t(".invite_settings.require_invite_for_signup") %>

+

<%= t(".invite_settings.invite_code_description") %>

+
+ + <%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %> +
+ <%= form.check_box :require_invite_for_signup, class: "sr-only peer", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "input" %> + <%= form.label :require_invite_for_signup, " ".html_safe, class: "maybe-switch" %> +
+ <% end %> +
+ + <% if Setting.require_invite_for_signup %> +
+
+ <%= t(".invite_settings.generated_tokens") %> +
+
+ <%= button_to invite_codes_path, + method: :post, + class: "flex gap-1 bg-gray-50 text-gray-900 text-sm rounded-lg px-3 py-2" do %> + <%= t(".invite_settings.generate_tokens") %> + <% end %> +
+
+ +
+ <%= turbo_frame_tag :invite_codes, src: invite_codes_path %> +
+ <% end %> +
+ <% end %> + <%= settings_nav_footer %>
diff --git a/config/locales/views/invite_codes/en.yml b/config/locales/views/invite_codes/en.yml new file mode 100644 index 00000000..8517ae82 --- /dev/null +++ b/config/locales/views/invite_codes/en.yml @@ -0,0 +1,7 @@ +--- +en: + invite_codes: + index: + invite_code_description: Generate a new code to see it displayed here. Generated + codes that have been used will no longer be shown. + no_invite_codes: No codes to show diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml index 5484cf82..700ec20a 100644 --- a/config/locales/views/settings/en.yml +++ b/config/locales/views/settings/en.yml @@ -12,6 +12,13 @@ en: email_sender: Email Sender email_sender_placeholder: user@mydomain.com general_settings_title: General Settings + invite_settings: + generate_tokens: Generate new code + generated_tokens: Generated codes + invite_code_description: Every new user that joins your instance if Maybe + can only do so via an invite code + require_invite_for_signup: Require invite code for new sign ups + title: Invite Codes page_title: Self-Hosting provider_settings: title: Provider Settings diff --git a/config/routes.rb b/config/routes.rb index 63380659..9acbdbe1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -96,6 +96,7 @@ Rails.application.routes.draw do end resources :institutions, except: %i[index show] + resources :invite_codes, only: %i[index create] resources :issues, only: :show diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb index 3200672b..10d09663 100644 --- a/test/system/settings_test.rb +++ b/test/system/settings_test.rb @@ -31,6 +31,21 @@ class SettingsTest < ApplicationSystemTestCase end end + test "can update self hosting settings" do + Rails.application.config.app_mode.stubs(:self_hosted?).returns(true) + open_settings_from_sidebar + assert_selector "li", text: "Self hosting" + 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 + 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) + copy_button.click + assert_selector 'span[data-clipboard-target="iconSuccess"]', visible: true, count: 1 # text copied and icon changed to checkmark + end + private def open_settings_from_sidebar