diff --git a/app/views/cards/_messages.html.erb b/app/views/cards/_messages.html.erb
index a2784e72a..c51ab6d8b 100644
--- a/app/views/cards/_messages.html.erb
+++ b/app/views/cards/_messages.html.erb
@@ -1,7 +1,11 @@
<%= messages_tag(card) do %>
<% if card.published? %>
<%= render partial: "cards/comments/comment", collection: card.comments.preloaded.chronologically, cached: true %>
- <%= render "cards/comments/new", card: card %>
+ <% if Fizzy.saas? %>
+ <%= render "cards/comments/saas/new", card: card %>
+ <% else %>
+ <%= render "cards/comments/new", card: card %>
+ <% end %>
<%= render "cards/comments/watchers", card: card %>
<% end %>
diff --git a/app/views/cards/container/footer/_create.html.erb b/app/views/cards/container/footer/_create.html.erb
index 648beffd1..4e5a83e20 100644
--- a/app/views/cards/container/footer/_create.html.erb
+++ b/app/views/cards/container/footer/_create.html.erb
@@ -13,5 +13,7 @@
Create and add another
<% end %>
+
+ <%= render "cards/container/footer/saas/storage_limit_notice" if Fizzy.saas? %>
diff --git a/app/views/cards/drafts/_container.html.erb b/app/views/cards/drafts/_container.html.erb
index 9ffc04711..fa7134b55 100644
--- a/app/views/cards/drafts/_container.html.erb
+++ b/app/views/cards/drafts/_container.html.erb
@@ -26,5 +26,9 @@
<% end %>
- <%= render "cards/container/footer/create", card: card %>
+ <% if Fizzy.saas? %>
+ <%= render "cards/container/footer/saas/create", card: card %>
+ <% else %>
+ <%= render "cards/container/footer/create", card: card %>
+ <% end %>
diff --git a/saas/app/controllers/concerns/card/storage_limited.rb b/saas/app/controllers/concerns/card/storage_limited.rb
new file mode 100644
index 000000000..05357f43f
--- /dev/null
+++ b/saas/app/controllers/concerns/card/storage_limited.rb
@@ -0,0 +1,8 @@
+module Card::StorageLimited
+ extend ActiveSupport::Concern
+
+ private
+ def ensure_within_storage_limit
+ head :forbidden if Current.account.exceeding_storage_limit?
+ end
+end
diff --git a/saas/app/controllers/concerns/card/storage_limited/commenting.rb b/saas/app/controllers/concerns/card/storage_limited/commenting.rb
new file mode 100644
index 000000000..a4b7e3307
--- /dev/null
+++ b/saas/app/controllers/concerns/card/storage_limited/commenting.rb
@@ -0,0 +1,9 @@
+module Card::StorageLimited::Commenting
+ extend ActiveSupport::Concern
+
+ included do
+ include Card::StorageLimited
+
+ before_action :ensure_within_storage_limit, only: :create
+ end
+end
diff --git a/saas/app/controllers/concerns/card/storage_limited/creation.rb b/saas/app/controllers/concerns/card/storage_limited/creation.rb
new file mode 100644
index 000000000..9d894779d
--- /dev/null
+++ b/saas/app/controllers/concerns/card/storage_limited/creation.rb
@@ -0,0 +1,9 @@
+module Card::StorageLimited::Creation
+ extend ActiveSupport::Concern
+
+ included do
+ include Card::StorageLimited
+
+ before_action :ensure_within_storage_limit, only: :create, if: -> { request.format.json? }
+ end
+end
diff --git a/saas/app/controllers/concerns/card/storage_limited/publishing.rb b/saas/app/controllers/concerns/card/storage_limited/publishing.rb
new file mode 100644
index 000000000..abf7731a4
--- /dev/null
+++ b/saas/app/controllers/concerns/card/storage_limited/publishing.rb
@@ -0,0 +1,9 @@
+module Card::StorageLimited::Publishing
+ extend ActiveSupport::Concern
+
+ included do
+ include Card::StorageLimited
+
+ before_action :ensure_within_storage_limit, only: :create
+ end
+end
diff --git a/saas/app/models/account/storage_limited.rb b/saas/app/models/account/storage_limited.rb
new file mode 100644
index 000000000..dbab32b5d
--- /dev/null
+++ b/saas/app/models/account/storage_limited.rb
@@ -0,0 +1,14 @@
+module Account::StorageLimited
+ extend ActiveSupport::Concern
+
+ STORAGE_LIMIT = 1.gigabyte
+ NEAR_STORAGE_LIMIT_THRESHOLD = 500.megabytes
+
+ def exceeding_storage_limit?
+ bytes_used > STORAGE_LIMIT
+ end
+
+ def nearing_storage_limit?
+ !exceeding_storage_limit? && bytes_used > STORAGE_LIMIT - NEAR_STORAGE_LIMIT_THRESHOLD
+ end
+end
diff --git a/saas/app/views/cards/comments/saas/_new.html.erb b/saas/app/views/cards/comments/saas/_new.html.erb
new file mode 100644
index 000000000..74cb6dd43
--- /dev/null
+++ b/saas/app/views/cards/comments/saas/_new.html.erb
@@ -0,0 +1,5 @@
+<% if Current.account.exceeding_storage_limit? %>
+ <%= render "cards/comments/saas/storage_limit_exceeded" %>
+<% else %>
+ <%= render "cards/comments/new", card: card %>
+<% end %>
diff --git a/saas/app/views/cards/comments/saas/_storage_limit_exceeded.html.erb b/saas/app/views/cards/comments/saas/_storage_limit_exceeded.html.erb
new file mode 100644
index 000000000..6be5fb587
--- /dev/null
+++ b/saas/app/views/cards/comments/saas/_storage_limit_exceeded.html.erb
@@ -0,0 +1,4 @@
+
+ This account has used all <%= number_to_human_size(Account::StorageLimited::STORAGE_LIMIT) %> of free storage.
+ <%= link_to "Self-host Fizzy", "https://github.com/basecamp/fizzy", target: "_blank" %> for unlimited storage.
+
diff --git a/saas/app/views/cards/container/footer/saas/_create.html.erb b/saas/app/views/cards/container/footer/saas/_create.html.erb
new file mode 100644
index 000000000..e5544e576
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/_create.html.erb
@@ -0,0 +1,5 @@
+<% if Current.account.exceeding_storage_limit? %>
+ <%= render "cards/container/footer/saas/storage_limit_exceeded" %>
+<% else %>
+ <%= render "cards/container/footer/create", card: card %>
+<% end %>
diff --git a/saas/app/views/cards/container/footer/saas/_storage_limit_exceeded.html.erb b/saas/app/views/cards/container/footer/saas/_storage_limit_exceeded.html.erb
new file mode 100644
index 000000000..6fb95be11
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/_storage_limit_exceeded.html.erb
@@ -0,0 +1,8 @@
+
+
+
This account has used all <%= number_to_human_size(Account::StorageLimited::STORAGE_LIMIT) %> of free storage.
+
+ <%= link_to "Self-host Fizzy", "https://github.com/basecamp/fizzy", target: "_blank" %> for unlimited storage.
+
+
+
diff --git a/saas/app/views/cards/container/footer/saas/_storage_limit_notice.html.erb b/saas/app/views/cards/container/footer/saas/_storage_limit_notice.html.erb
new file mode 100644
index 000000000..942041fb4
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/_storage_limit_notice.html.erb
@@ -0,0 +1,5 @@
+<% if Current.account.nearing_storage_limit? %>
+
+ This account has used <%= number_to_human_size(Current.account.bytes_used) %> of <%= number_to_human_size(Account::StorageLimited::STORAGE_LIMIT) %> storage.
+
+<% end %>
diff --git a/saas/lib/fizzy/saas/engine.rb b/saas/lib/fizzy/saas/engine.rb
index dea321cfc..673be1147 100644
--- a/saas/lib/fizzy/saas/engine.rb
+++ b/saas/lib/fizzy/saas/engine.rb
@@ -133,10 +133,14 @@ module Fizzy
end
config.to_prepare do
+ ::Account.include Account::StorageLimited
::Identity.include Authorization::Identity, Identity::Devices
::Session.include Session::Devices
::Signup.prepend Signup
ApplicationController.include Authorization::Controller
+ CardsController.include(Card::StorageLimited::Creation)
+ Cards::CommentsController.include(Card::StorageLimited::Commenting)
+ Cards::PublishesController.include(Card::StorageLimited::Publishing)
Notification.register_push_target(:native)
diff --git a/saas/test/controllers/card/storage_limited/commenting_test.rb b/saas/test/controllers/card/storage_limited/commenting_test.rb
new file mode 100644
index 000000000..e32015130
--- /dev/null
+++ b/saas/test/controllers/card/storage_limited/commenting_test.rb
@@ -0,0 +1,29 @@
+require "test_helper"
+
+class Card::StorageLimited::CommentingTest < ActionDispatch::IntegrationTest
+ test "cannot create comments when storage limit exceeded" do
+ sign_in_as :david
+
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ assert_no_difference -> { Comment.count } do
+ post card_comments_path(cards(:logo), script_name: accounts(:"37s").slug),
+ params: { comment: { body: "Blocked comment" } },
+ as: :turbo_stream
+ end
+
+ assert_response :forbidden
+ end
+
+ test "can create comments when under storage limit" do
+ sign_in_as :david
+
+ assert_difference -> { Comment.count } do
+ post card_comments_path(cards(:logo), script_name: accounts(:"37s").slug),
+ params: { comment: { body: "Allowed comment" } },
+ as: :turbo_stream
+ end
+
+ assert_response :success
+ end
+end
diff --git a/saas/test/controllers/card/storage_limited/creation_test.rb b/saas/test/controllers/card/storage_limited/creation_test.rb
new file mode 100644
index 000000000..ab04774fe
--- /dev/null
+++ b/saas/test/controllers/card/storage_limited/creation_test.rb
@@ -0,0 +1,47 @@
+require "test_helper"
+
+class Card::StorageLimited::CreationTest < ActionDispatch::IntegrationTest
+ test "cannot create cards via JSON when storage limit exceeded" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ assert_no_difference -> { Card.count } do
+ post board_cards_path(boards(:miltons_wish_list), script_name: accounts(:initech).slug),
+ params: { card: { title: "Blocked card" } },
+ as: :json
+ end
+
+ assert_response :forbidden
+ end
+
+ test "can create cards via HTML when storage limit exceeded since they become drafts" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+ accounts(:initech).update_column(:cards_count, 100)
+ boards(:miltons_wish_list).cards.drafted.where(creator: users(:mike)).destroy_all
+
+ assert_difference -> { Card.count } do
+ post board_cards_path(boards(:miltons_wish_list), script_name: accounts(:initech).slug)
+ end
+
+ assert_response :redirect
+ assert Card.last.drafted?
+ end
+
+ test "can create cards via JSON when under storage limit" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(500.megabytes)
+ accounts(:initech).update_column(:cards_count, 100)
+
+ assert_difference -> { Card.count } do
+ post board_cards_path(boards(:miltons_wish_list), script_name: accounts(:initech).slug),
+ params: { card: { title: "Allowed card" } },
+ as: :json
+ end
+
+ assert_response :created
+ end
+end
diff --git a/saas/test/controllers/card/storage_limited/publishing_test.rb b/saas/test/controllers/card/storage_limited/publishing_test.rb
new file mode 100644
index 000000000..94e9b8e4f
--- /dev/null
+++ b/saas/test/controllers/card/storage_limited/publishing_test.rb
@@ -0,0 +1,23 @@
+require "test_helper"
+
+class Card::StorageLimited::PublishingTest < ActionDispatch::IntegrationTest
+ test "cannot publish cards when storage limit exceeded" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ post card_publish_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :forbidden
+ assert cards(:unfinished_thoughts).reload.drafted?
+ end
+
+ test "can publish cards when under storage limit" do
+ sign_in_as :mike
+
+ post card_publish_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :redirect
+ assert cards(:unfinished_thoughts).reload.published?
+ end
+end
diff --git a/saas/test/controllers/card/storage_limited_test.rb b/saas/test/controllers/card/storage_limited_test.rb
new file mode 100644
index 000000000..69790c363
--- /dev/null
+++ b/saas/test/controllers/card/storage_limited_test.rb
@@ -0,0 +1,27 @@
+require "test_helper"
+
+class Card::StorageLimitedTest < ActionDispatch::IntegrationTest
+ test "draft card shows storage limit notice instead of create buttons when limit exceeded" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_select ".card-perma__notch" do
+ assert_select "strong", text: /used all/
+ assert_select "a[href='https://github.com/basecamp/fizzy']", text: "Self-host Fizzy"
+ end
+ assert_select ".card-perma__notch-new-card-buttons", count: 0
+ end
+
+ test "draft card shows create buttons when under storage limit" do
+ sign_in_as :mike
+
+ get card_draft_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_select ".card-perma__notch-new-card-buttons"
+ end
+end
diff --git a/saas/test/controllers/comment/storage_limited_test.rb b/saas/test/controllers/comment/storage_limited_test.rb
new file mode 100644
index 000000000..4137971b1
--- /dev/null
+++ b/saas/test/controllers/comment/storage_limited_test.rb
@@ -0,0 +1,25 @@
+require "test_helper"
+
+class Comment::StorageLimitedTest < ActionDispatch::IntegrationTest
+ test "published card shows storage limit notice instead of comment form when limit exceeded" do
+ sign_in_as :david
+
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ get card_path(cards(:logo), script_name: accounts(:"37s").slug)
+
+ assert_response :success
+ assert_select "strong", text: /used all/
+ assert_select "a[href='https://github.com/basecamp/fizzy']", text: "Self-host Fizzy"
+ assert_select "##{dom_id(cards(:logo), :new_comment)}", count: 0
+ end
+
+ test "published card shows comment form when under storage limit" do
+ sign_in_as :david
+
+ get card_path(cards(:logo), script_name: accounts(:"37s").slug)
+
+ assert_response :success
+ assert_select "##{dom_id(cards(:logo), :new_comment)}"
+ end
+end
diff --git a/saas/test/models/account/storage_limited_test.rb b/saas/test/models/account/storage_limited_test.rb
new file mode 100644
index 000000000..f587d9ef1
--- /dev/null
+++ b/saas/test/models/account/storage_limited_test.rb
@@ -0,0 +1,39 @@
+require "test_helper"
+
+class Account::StorageLimitedTest < ActiveSupport::TestCase
+ test "exceeding storage limit when bytes used exceeds 1 GB" do
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ assert accounts(:initech).exceeding_storage_limit?
+ end
+
+ test "not exceeding storage limit when bytes used equals 1 GB" do
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte)
+
+ assert_not accounts(:initech).exceeding_storage_limit?
+ end
+
+ test "not exceeding storage limit when under 1 GB" do
+ Account.any_instance.stubs(:bytes_used).returns(500.megabytes)
+
+ assert_not accounts(:initech).exceeding_storage_limit?
+ end
+
+ test "nearing storage limit when within 500 MB of the limit" do
+ Account.any_instance.stubs(:bytes_used).returns(600.megabytes)
+
+ assert accounts(:initech).nearing_storage_limit?
+ end
+
+ test "not nearing storage limit when well under the threshold" do
+ Account.any_instance.stubs(:bytes_used).returns(400.megabytes)
+
+ assert_not accounts(:initech).nearing_storage_limit?
+ end
+
+ test "not nearing storage limit when already exceeding it" do
+ Account.any_instance.stubs(:bytes_used).returns(1.gigabyte + 1)
+
+ assert_not accounts(:initech).nearing_storage_limit?
+ end
+end