Merge pull request #2035 from harisadam/single_tenant

Add env-configurable option to disable public signups
This commit is contained in:
Kevin McConnell
2025-12-11 13:08:19 +00:00
committed by GitHub
15 changed files with 98 additions and 5 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ GIT
GIT
remote: https://github.com/basecamp/fizzy-saas
revision: a14df11b57818697df4b2cc7b6a43e762ebaa196
revision: fb30201b9b2752f9e9cff2f9f36b698b13856404
specs:
fizzy-saas (0.1.0)
audits1984
+2
View File
@@ -38,6 +38,8 @@ We've added comments to that file to highlight what each setting needs to be, bu
- `proxy/ssl` and `proxy/host`: Kamal can set up SSL certificates for you automatically. To enable that, set the hostname again as `host`. If you don't want SSL for some reason, you can set `ssl: false` to turn it off.
- `env/clear/MAILER_FROM_ADDRESS`: This is the email address that Fizzy will send emails from. It should usually be an address from the same domain where you're running Fizzy.
- `env/clear/SMTP_ADDRESS`: The address of an SMTP server that you can send email through. You can use a 3rd-party service for this, like Sendgrid or Postmark, in which case their documentation will tell you what to use for this.
- `env/clear/MULTI_TENANT`: Set to `false` to enable single-tenant mode (disable multi-account signups).
Fizzy also requires a few environment variables to be set up, some of which contain secrets.
The simplest way to do this is to put them in a file called `.kamal/secrets`.
+2 -1
View File
@@ -14,7 +14,8 @@ class SessionsController < ApplicationController
else
signup = Signup.new(email_address: email_address)
if signup.valid?(:identity_creation)
redirect_to_session_magic_link signup.create_identity
identity = signup.create_identity if Account.accepting_signups?
redirect_to_session_magic_link identity
else
head :unprocessable_entity
end
+5
View File
@@ -3,6 +3,7 @@ class SignupsController < ApplicationController
allow_unauthenticated_access
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signup_path, alert: "Try again later." }
before_action :redirect_authenticated_user
before_action :enforce_tenant_limit
layout "public"
@@ -24,6 +25,10 @@ class SignupsController < ApplicationController
redirect_to new_signup_completion_path if authenticated?
end
def enforce_tenant_limit
redirect_to new_session_url unless Account.accepting_signups?
end
def signup_params
params.expect signup: :email_address
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Account < ApplicationRecord
include Account::Storage, Entropic, Seedeable
include Account::Storage, Entropic, MultiTenantable, Seedeable
has_one :join_code
has_many :users, dependent: :destroy
+13
View File
@@ -0,0 +1,13 @@
module Account::MultiTenantable
extend ActiveSupport::Concern
included do
cattr_accessor :multi_tenant, default: false
end
class_methods do
def accepting_signups?
multi_tenant || Account.none?
end
end
end
+3 -1
View File
@@ -3,7 +3,9 @@
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] || flash[:shake] %>">
<header>
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<p class="margin-none-block-start txt-medium">Then enter the verification code included in the email below:</p>
<p class="margin-none-block-start txt-medium">
Then enter the verification code included in the email below:
</p>
</header>
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: "magic-link" } } do |form| %>
+5 -1
View File
@@ -10,7 +10,11 @@
</label>
</div>
<p><strong>New here?</strong> <%= link_to "Sign up", new_signup_path %> to create an account. <strong>Already have an account?</strong> Enter your email and well get you signed in.</p>
<% if Account.accepting_signups? %>
<p><strong>New here?</strong> <%= link_to "Sign up", new_signup_path %> to create an account. <strong>Already have an account?</strong> Enter your email and well get you signed in.</p>
<% else %>
<p>Enter your email and well get you signed in.</p>
<% end %>
<button type="submit" id="log_in" class="btn btn--link center txt-medium">
<span>Lets go</span>
+1
View File
@@ -30,6 +30,7 @@ env:
clear:
MAILER_FROM_ADDRESS: support@example.com # The email "from" address that Fizzy sends email from
SMTP_ADDRESS: mail.example.com # The SMTP server you'll use to send email
MULTI_TENANT: false # Set to false to enable single-tenant mode
SOLID_QUEUE_IN_PUMA: true # Run background jobs in the app container
+3
View File
@@ -67,4 +67,7 @@ Rails.application.configure do
# Load test helpers
config.autoload_paths += %w[ test/test_helpers ]
# Enable multi-tenant mode for tests
config.x.multi_tenant.enabled = true
end
+5
View File
@@ -0,0 +1,5 @@
Rails.application.configure do
config.after_initialize do
Account.multi_tenant = ENV["MULTI_TENANT"] == "true" || config.x.multi_tenant.enabled == true
end
end
@@ -36,6 +36,21 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
end
end
test "create for a new user when single tenant mode already has a tenant" do
with_multi_tenant_mode(false) do
untenanted do
assert_no_difference -> { MagicLink.count } do
assert_no_difference -> { Identity.count } do
post session_path,
params: { email_address: "nonexistent-#{SecureRandom.hex(6)}@example.com" }
end
end
assert_redirected_to session_magic_link_path
end
end
end
test "create with invalid email address" do
# Avoid Sentry exceptions when attackers try to stuff invalid emails. The browser performs form
# field validation that should normally prevent this from occurring, so I'm not worried about
@@ -63,4 +63,16 @@ class SignupsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to new_signup_completion_path
end
end
test "redirects to session#new when single_tenant and user exists" do
users(:david)
with_multi_tenant_mode(false) do
untenanted do
get new_signup_path
assert_redirected_to new_session_url
end
end
end
end
@@ -0,0 +1,22 @@
require "test_helper"
class Account::MultiTenantableTest < ActiveSupport::TestCase
test "accepting_signups? is true when multi_tenant is enabled" do
with_multi_tenant_mode(true) do
assert Account.accepting_signups?
end
end
test "accepting_signups? is false when multi_tenant is disabled and accounts exist" do
with_multi_tenant_mode(false) do
assert_not Account.accepting_signups?
end
end
test "accepting_signups? is true when multi_tenant is disabled but no accounts exist" do
with_multi_tenant_mode(false) do
Account.delete_all
assert Account.accepting_signups?
end
end
end
+8
View File
@@ -57,4 +57,12 @@ module SessionTestHelper
ensure
integration_session.default_url_options[:script_name] = original_script_name
end
def with_multi_tenant_mode(enabled)
previous = Account.multi_tenant
Account.multi_tenant = enabled
yield
ensure
Account.multi_tenant = previous
end
end