mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Add setting to disable new user registration on self-hosted instances (#1163)
* Add clipboard stimulus controller * Add invite codes controller * Setting to force invite code for new signups * Fix erb linter * Normalize keys * Add POST /invite_codes * Cleanup clipboard_controller.js * Create invite codes on-demand * Design changes * Style alignment * Update app/views/invite_codes/_invite_code.html.erb Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com> Signed-off-by: Tony Vincent <tonyvince7@gmail.com> * Update app/views/invite_codes/_invite_code.html.erb Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com> Signed-off-by: Tony Vincent <tonyvince7@gmail.com> * Split into individual forms * Fix missing styles * Update app/javascript/controllers/clipboard_controller.js Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com> Signed-off-by: Tony Vincent <tonyvince7@gmail.com> * Fix test --------- Signed-off-by: Tony Vincent <tonyvince7@gmail.com> Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
This commit is contained in:
parent
5178928b68
commit
edf44bec03
11 changed files with 197 additions and 62 deletions
10
app/controllers/invite_codes_controller.rb
Normal file
10
app/controllers/invite_codes_controller.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
28
app/javascript/controllers/clipboard_controller.js
Normal file
28
app/javascript/controllers/clipboard_controller.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
|
|
16
app/views/invite_codes/_invite_code.html.erb
Normal file
16
app/views/invite_codes/_invite_code.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<%# app/views/invite_codes/_invite_code.html.erb %>
|
||||
<div class="invite_code pt-2">
|
||||
<div class="flex items-center justify-between p-2 w-1/2 bg-gray-25 rounded-md" data-controller="clipboard">
|
||||
<div>
|
||||
<span data-clipboard-target="source" class="text-sm font-medium"><%= invite_code.token %></span>
|
||||
</div>
|
||||
<button data-action="clipboard#copy" class="flex-shrink-0 z-10 inline-flex items-center px-1 text-sm text-gray-500 font-sm text-center" type="button">
|
||||
<span data-clipboard-target="iconDefault">
|
||||
<%= lucide_icon "copy", class: "w-5 h-5" %>
|
||||
</span>
|
||||
<span class="hidden inline-flex items-center" data-clipboard-target="iconSuccess">
|
||||
<%= lucide_icon "check", class: "w-5 h-4" %>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
12
app/views/invite_codes/index.html.erb
Normal file
12
app/views/invite_codes/index.html.erb
Normal file
|
@ -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 %>
|
||||
<div class="flex flex-col items-center w-full h-64 bg-white text-center justify-center">
|
||||
<%= lucide_icon "binary", class: "w-6 h-6 text-sm text-gray-500" %>
|
||||
<p class="text-base pt-4"><%= t(".no_invite_codes") %></p>
|
||||
<p class="text-sm text-gray-500 pt-2 w-2/3"><%= t(".invite_code_description") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -2,46 +2,45 @@
|
|||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4 pb-32">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= 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" %>
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= 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 %>
|
||||
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.manual.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= 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 %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.latest_release.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= 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 %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.latest_commit.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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| %>
|
||||
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= 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 %>
|
||||
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.manual.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= 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 %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.latest_release.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= 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 %>
|
||||
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
|
||||
<br>
|
||||
<span class="text-gray-500">
|
||||
<%= t(".upgrades.latest_commit.description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".smtp_settings.title") %></h2>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<%= 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" } %>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-white border border-alpha-black-100 p-4 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-25 flex items-center justify-center">
|
||||
<%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-900 font-medium text-sm"><%= t(".smtp_settings.send_test_email") %></p>
|
||||
<p class="text-gray-500 text-sm"><%= t(".smtp_settings.send_test_email_description") %></p>
|
||||
</div>
|
||||
<%= 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| %>
|
||||
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<%= 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" } %>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-white border border-alpha-black-100 p-4 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-25 flex items-center justify-center">
|
||||
<%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= 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" %>
|
||||
<p class="text-gray-900 font-medium text-sm"><%= t(".smtp_settings.send_test_email") %></p>
|
||||
<p class="text-gray-500 text-sm"><%= t(".smtp_settings.send_test_email_description") %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= settings_section title: t(".invite_settings.title") do %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm"><%= t(".invite_settings.require_invite_for_signup") %></p>
|
||||
<p class="text-gray-500 text-sm"><%= t(".invite_settings.invite_code_description") %></p>
|
||||
</div>
|
||||
|
||||
<%= 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| %>
|
||||
<div class="relative inline-block select-none">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if Setting.require_invite_for_signup %>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<span class="text-gray-900 text-base font-medium"><%= t(".invite_settings.generated_tokens") %></span>
|
||||
</div>
|
||||
<div>
|
||||
<%= 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 %>
|
||||
<span><%= t(".invite_settings.generate_tokens") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= turbo_frame_tag :invite_codes, src: invite_codes_path %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= settings_nav_footer %>
|
||||
</div>
|
||||
|
|
7
config/locales/views/invite_codes/en.yml
Normal file
7
config/locales/views/invite_codes/en.yml
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue