diff --git a/Gemfile b/Gemfile
index 1532614..6820e1d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,9 +4,11 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Rails
gem "rails", github: "rails/rails", branch: "main"
+gem "ostruct"
+gem "benchmark"
# Drivers
-gem "sqlite3", "~> 2.7"
+gem "sqlite3"
gem "redis", "~> 5.4"
# Deployment
diff --git a/Gemfile.lock b/Gemfile.lock
index 30b671e..75a617f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -135,6 +135,7 @@ GEM
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.20)
+ benchmark (0.5.0)
bigdecimal (3.3.1)
brakeman (7.1.1)
racc
@@ -239,6 +240,7 @@ GEM
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
openssl (3.3.0)
+ ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
@@ -408,6 +410,7 @@ PLATFORMS
DEPENDENCIES
bcrypt
+ benchmark
brakeman
capybara
debug
@@ -419,6 +422,7 @@ DEPENDENCIES
kredis
mocha
net-http-persistent
+ ostruct
platform_agent
propshaft!
puma (~> 6.6)
@@ -432,7 +436,7 @@ DEPENDENCIES
selenium-webdriver
sentry-rails
sentry-ruby
- sqlite3 (~> 2.7)
+ sqlite3
stimulus-rails
thruster
turbo-rails!
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index be05d66..462ae01 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -17,7 +17,7 @@ class AccountsController < ApplicationController
end
def account_params
- params.require(:account).permit(:name, :logo)
+ params.require(:account).permit(:name, :logo, settings: {})
end
def account_users
diff --git a/app/controllers/rooms/closeds_controller.rb b/app/controllers/rooms/closeds_controller.rb
index c6aa7f2..0e8bdf7 100644
--- a/app/controllers/rooms/closeds_controller.rb
+++ b/app/controllers/rooms/closeds_controller.rb
@@ -3,6 +3,7 @@ class Rooms::ClosedsController < RoomsController
before_action :ensure_can_administer, only: %i[ update ]
before_action :remember_last_room_visited, only: :show
before_action :force_room_type, only: %i[ edit update ]
+ before_action :ensure_permission_to_create_rooms, only: %i[ new create ]
DEFAULT_ROOM_NAME = "New room"
diff --git a/app/controllers/rooms/opens_controller.rb b/app/controllers/rooms/opens_controller.rb
index 5f40138..c2bda9e 100644
--- a/app/controllers/rooms/opens_controller.rb
+++ b/app/controllers/rooms/opens_controller.rb
@@ -3,6 +3,7 @@ class Rooms::OpensController < RoomsController
before_action :ensure_can_administer, only: %i[ update ]
before_action :remember_last_room_visited, only: :show
before_action :force_room_type, only: %i[ edit update ]
+ before_action :ensure_permission_to_create_rooms, only: %i[ new create ]
DEFAULT_ROOM_NAME = "New room"
diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb
index b2d8142..2c308c8 100644
--- a/app/controllers/rooms_controller.rb
+++ b/app/controllers/rooms_controller.rb
@@ -31,6 +31,12 @@ class RoomsController < ApplicationController
head :forbidden unless Current.user.can_administer?(@room)
end
+ def ensure_permission_to_create_rooms
+ if Current.account.settings.restrict_room_creation_to_administrators? && !Current.user.administrator?
+ head :forbidden
+ end
+ end
+
def find_messages
messages = @room.messages.with_creator.with_attachment_details.with_boosts
diff --git a/app/models/account.rb b/app/models/account.rb
index 36a7648..f0b7c98 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -2,4 +2,5 @@ class Account < ApplicationRecord
include Joinable
has_one_attached :logo
+ has_json :settings, restrict_room_creation_to_administrators: false
end
diff --git a/app/models/webhook.rb b/app/models/webhook.rb
index 1afefa6..d8e7b43 100644
--- a/app/models/webhook.rb
+++ b/app/models/webhook.rb
@@ -53,7 +53,7 @@ class Webhook < ApplicationRecord
end
def extract_text_from(response)
- response.body.dup.force_encoding("UTF-8") if response.code == "200" && response.content_type.in?(%w[ text/html text/plain ])
+ String.new(response.body).force_encoding("UTF-8") if response.code == "200" && response.content_type.in?(%w[ text/html text/plain ])
end
def receive_text_reply_to(room, text:)
diff --git a/app/views/accounts/edit.html.erb b/app/views/accounts/edit.html.erb
index 93a828a..1f9f868 100644
--- a/app/views/accounts/edit.html.erb
+++ b/app/views/accounts/edit.html.erb
@@ -64,6 +64,30 @@
<% end %>
<% end %>
+
+
+ <%= form_with model: @account, method: :put, data: { controller: "form" }, class: "flex align-center gap center" do |form| %>
+
+ <%= image_tag "crown.svg", class: "colorize--black", aria: { hidden: "true" }, size: 18 %> Must be admin to create new rooms
+
+ <%= form.fields_for :settings, @account.settings do |settings_form| %>
+ <%= settings_form.hidden_field :restrict_room_creation_to_administrators,
+ value: !Current.account.settings.restrict_room_creation_to_administrators? %>
+ <% end %>
+
+
+
+ <% end %>
+
<% else %>
<%= account_logo_tag style: "txt-xx-large center" %>
<%= @account.name %>
diff --git a/app/views/users/sidebars/show.html.erb b/app/views/users/sidebars/show.html.erb
index 35ab9b1..709c310 100644
--- a/app/views/users/sidebars/show.html.erb
+++ b/app/views/users/sidebars/show.html.erb
@@ -36,8 +36,10 @@
<% end %>
- <%= link_to new_rooms_open_path, class: "rooms__new-btn btn room align-center gap txt-reversed", aria: { label: "New Chat Room" } do %>
- <%= image_tag "add.svg", size: 20, aria: { hidden: "true" }, style: "view-transition-name: new-room" %>
+ <% if Current.user.administrator? || !Current.account.settings.restrict_room_creation_to_administrators? %>
+ <%= link_to new_rooms_open_path, class: "rooms__new-btn btn room align-center gap txt-reversed", aria: { label: "New Chat Room" } do %>
+ <%= image_tag "add.svg", size: 20, aria: { hidden: "true" }, style: "view-transition-name: new-room" %>
+ <% end %>
<% end %>
diff --git a/db/migrate/20251126130131_add_account_settings.rb b/db/migrate/20251126130131_add_account_settings.rb
new file mode 100644
index 0000000..375f53d
--- /dev/null
+++ b/db/migrate/20251126130131_add_account_settings.rb
@@ -0,0 +1,5 @@
+class AddAccountSettings < ActiveRecord::Migration[7.2]
+ def change
+ add_column :accounts, :settings, :json
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 07e0f23..658630d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,12 +10,13 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2025_11_26_115722) do
+ActiveRecord::Schema[8.2].define(version: 2025_11_26_130131) do
create_table "accounts", force: :cascade do |t|
t.datetime "created_at", null: false
t.text "custom_styles"
t.string "join_code", null: false
t.string "name", null: false
+ t.json "settings"
t.datetime "updated_at", null: false
end
diff --git a/test/controllers/rooms/closeds_controller_test.rb b/test/controllers/rooms/closeds_controller_test.rb
index 5172453..d8976ac 100644
--- a/test/controllers/rooms/closeds_controller_test.rb
+++ b/test/controllers/rooms/closeds_controller_test.rb
@@ -29,6 +29,16 @@ class Rooms::ClosedsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to room_url(Room.last)
end
+ test "create forbidden by non-admin when account restricts creation to admins" do
+ accounts(:signal).settings.restrict_room_creation_to_administrators = true
+ accounts(:signal).save!
+
+ sign_in :jz
+ post rooms_closeds_url, params: { room: { name: "My New Room" }, user_ids: [ users(:david).id, users(:kevin).id, users(:jason).id ] }
+ assert_response :forbidden
+ end
+
+
test "update with membership revisions" do
assert_difference -> { rooms(:designers).reload.users.count }, -1 do
put rooms_closed_url(rooms(:designers)), params: {
diff --git a/test/controllers/rooms/opens_controller_test.rb b/test/controllers/rooms/opens_controller_test.rb
index 8820008..0f91294 100644
--- a/test/controllers/rooms/opens_controller_test.rb
+++ b/test/controllers/rooms/opens_controller_test.rb
@@ -24,6 +24,15 @@ class Rooms::OpensControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to room_url(Room.last)
end
+ test "create forbidden by non-admin when account restricts creation to admins" do
+ accounts(:signal).settings.restrict_room_creation_to_administrators = true
+ accounts(:signal).save!
+
+ sign_in :jz
+ post rooms_opens_url, params: { room: { name: "My New Room" } }
+ assert_response :forbidden
+ end
+
test "only admins or creators can update" do
sign_in :jz
diff --git a/test/models/account_test.rb b/test/models/account_test.rb
index 94d4f83..2585ba0 100644
--- a/test/models/account_test.rb
+++ b/test/models/account_test.rb
@@ -1,4 +1,18 @@
require "test_helper"
class AccountTest < ActiveSupport::TestCase
+ test "settings" do
+ accounts(:signal).settings.restrict_room_creation_to_administrators = true
+ assert accounts(:signal).settings.restrict_room_creation_to_administrators?
+ assert_equal({ "restrict_room_creation_to_administrators" => true }, accounts(:signal)[:settings])
+
+ accounts(:signal).update!(settings: { "restrict_room_creation_to_administrators" => "true" })
+ assert accounts(:signal).reload.settings.restrict_room_creation_to_administrators?
+
+ accounts(:signal).settings.restrict_room_creation_to_administrators = false
+ assert_not accounts(:signal).settings.restrict_room_creation_to_administrators?
+ assert_equal({ "restrict_room_creation_to_administrators" => false }, accounts(:signal)[:settings])
+ accounts(:signal).update!(settings: { "restrict_room_creation_to_administrators" => "false" })
+ assert_not accounts(:signal).reload.settings.restrict_room_creation_to_administrators?
+ end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 33a20be..9c1310c 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -5,6 +5,7 @@ require "rails/test_help"
require "minitest/unit"
require "mocha/minitest"
require "webmock/minitest"
+require "turbo/broadcastable/test_helper"
WebMock.enable!