mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 06:09:38 +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
|
end
|
||||||
|
|
||||||
def hosting_params
|
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 = {}
|
||||||
result[:upgrades_mode] = permitted_params[:upgrades_mode] == "manual" ? "manual" : "auto" if permitted_params.key?(:upgrades_mode)
|
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[: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[: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
|
result
|
||||||
end
|
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 :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
|
||||||
|
|
||||||
|
field :require_invite_for_signup, type: :boolean, default: false
|
||||||
|
|
||||||
scope :smtp_settings do
|
scope :smtp_settings do
|
||||||
field :smtp_host, type: :string, read_only: true, default: ENV["SMTP_ADDRESS"]
|
field :smtp_host, type: :string, read_only: true, default: ENV["SMTP_ADDRESS"]
|
||||||
field :smtp_port, type: :string, read_only: true, default: ENV["SMTP_PORT"]
|
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,15 +2,15 @@
|
||||||
<%= render "settings/nav" %>
|
<%= render "settings/nav" %>
|
||||||
<% end %>
|
<% 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>
|
<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" %>
|
<% if ENV["HOSTING_PLATFORM"] == "render" %>
|
||||||
<div>
|
<%= 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>
|
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
|
||||||
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
|
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center gap-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.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||||
|
@ -43,7 +43,6 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-medium mb-1"><%= t(".provider_settings.title") %></h2>
|
<h2 class="font-medium mb-1"><%= t(".provider_settings.title") %></h2>
|
||||||
|
@ -51,9 +50,11 @@
|
||||||
<%= 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" } %>
|
<%= 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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div>
|
<%= settings_section title: t(".smtp_settings.title") do %>
|
||||||
<h2 class="font-medium mb-1"><%= t(".smtp_settings.title") %></h2>
|
<%= 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>
|
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
|
@ -79,8 +80,44 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<% end %>
|
<% 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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= settings_nav_footer %>
|
<%= settings_nav_footer %>
|
||||||
|
|
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: Email Sender
|
||||||
email_sender_placeholder: user@mydomain.com
|
email_sender_placeholder: user@mydomain.com
|
||||||
general_settings_title: General Settings
|
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
|
page_title: Self-Hosting
|
||||||
provider_settings:
|
provider_settings:
|
||||||
title: Provider Settings
|
title: Provider Settings
|
||||||
|
|
|
@ -96,6 +96,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :institutions, except: %i[index show]
|
resources :institutions, except: %i[index show]
|
||||||
|
resources :invite_codes, only: %i[index create]
|
||||||
|
|
||||||
resources :issues, only: :show
|
resources :issues, only: :show
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,21 @@ class SettingsTest < ApplicationSystemTestCase
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def open_settings_from_sidebar
|
def open_settings_from_sidebar
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue