From e606a142f1cf6623d33c69f1bca9d9c50a462ff4 Mon Sep 17 00:00:00 2001 From: Dakota Chambers Date: Tue, 2 Dec 2025 15:25:50 -0600 Subject: [PATCH 01/82] Update board delete confirmation message --- app/views/boards/edit/_delete.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/boards/edit/_delete.html.erb b/app/views/boards/edit/_delete.html.erb index 7da3ee6c8..1fcc4696d 100644 --- a/app/views/boards/edit/_delete.html.erb +++ b/app/views/boards/edit/_delete.html.erb @@ -1,5 +1,5 @@ <%= form_with model: board, class: "txt-align-center margin-block-start-auto", method: :delete do |form| %> - <%= form.button class: "btn txt-negative borderless txt-small", data: { turbo_confirm: "Are you sure you want to permanently delete this board?" } do %> + <%= form.button class: "btn txt-negative borderless txt-small", data: { turbo_confirm: "Are you sure you want to permanently delete this board and all associated cards?" } do %> <%= icon_tag "trash" %> Delete this board <% end %> From 244d92d92a5c91e18053cf8aaa1b2758fa73d465 Mon Sep 17 00:00:00 2001 From: Ross van Zyl Date: Tue, 2 Dec 2025 22:22:24 -0500 Subject: [PATCH 02/82] hide "add a step" input when card is closed --- app/views/cards/display/perma/_steps.html.erb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/cards/display/perma/_steps.html.erb b/app/views/cards/display/perma/_steps.html.erb index f65892324..36ce68fc5 100644 --- a/app/views/cards/display/perma/_steps.html.erb +++ b/app/views/cards/display/perma/_steps.html.erb @@ -1,10 +1,12 @@
    <%= render partial: "cards/steps/step", collection: card.steps, as: :step %> -
  1. - - <%= form_with model: [card, Step.new], url: card_steps_path(card), class: "min-width", data: { controller: "form", action: "submit->form#preventEmptySubmit turbo:submit-end->form#reset" } do |form| %> - <%= form.text_field :content, class: "input step__content hide-focus-ring", placeholder: "Add a step…", autocomplete: "off", data: { form_target: "input", "1p-ignore": "true" }, aria: { label: "Add a step" } %> - <% end %> -
  2. + <% unless card.closed? %> +
  3. + + <%= form_with model: [card, Step.new], url: card_steps_path(card), class: "min-width", data: { controller: "form", action: "submit->form#preventEmptySubmit turbo:submit-end->form#reset" } do |form| %> + <%= form.text_field :content, class: "input step__content hide-focus-ring", placeholder: "Add a step…", autocomplete: "off", data: { form_target: "input", "1p-ignore": "true" }, aria: { label: "Add a step" } %> + <% end %> +
  4. + <% end %>
From 702865873d2020ad853e1cc1bcb70389a720b45b Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 3 Dec 2025 10:56:05 +0100 Subject: [PATCH 03/82] Allow only people who can administer a board to delete it --- app/controllers/boards_controller.rb | 2 +- test/controllers/boards_controller_test.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 1e4233696..ff7c35ec0 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -2,7 +2,7 @@ class BoardsController < ApplicationController include FilterScoped before_action :set_board, except: %i[ new create ] - before_action :ensure_permission_to_admin_board, only: %i[ update ] + before_action :ensure_permission_to_admin_board, only: %i[ update destroy ] def show if @filter.used?(ignore_boards: true) diff --git a/test/controllers/boards_controller_test.rb b/test/controllers/boards_controller_test.rb index 3729f09c7..816a2e7b3 100644 --- a/test/controllers/boards_controller_test.rb +++ b/test/controllers/boards_controller_test.rb @@ -136,4 +136,13 @@ class BoardsControllerTest < ActionDispatch::IntegrationTest assert_response :forbidden assert_equal original_name, board.reload.name end + + test "non-admin cannot destroy board they don't own" do + logout_and_sign_in_as :jz + + board = boards(:writebook) + delete board_path(board) + + assert_response :forbidden + end end From 77f11100204bd68e5a6b887ec000f124806279bd Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 3 Dec 2025 11:10:44 +0100 Subject: [PATCH 04/82] Allow only the owner of a reaction to delete it --- .../cards/comments/reactions_controller.rb | 7 ++++++- .../cards/comments/reactions_controller_test.rb | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/controllers/cards/comments/reactions_controller.rb b/app/controllers/cards/comments/reactions_controller.rb index 54f63dda7..5d5814503 100644 --- a/app/controllers/cards/comments/reactions_controller.rb +++ b/app/controllers/cards/comments/reactions_controller.rb @@ -15,7 +15,12 @@ class Cards::Comments::ReactionsController < ApplicationController def destroy @reaction = @comment.reactions.find(params[:id]) - @reaction.destroy + + if Current.user != @reaction.reacter + head :forbidden + else + @reaction.destroy + end end private diff --git a/test/controllers/cards/comments/reactions_controller_test.rb b/test/controllers/cards/comments/reactions_controller_test.rb index bf948743b..57d635e96 100644 --- a/test/controllers/cards/comments/reactions_controller_test.rb +++ b/test/controllers/cards/comments/reactions_controller_test.rb @@ -2,7 +2,7 @@ require "test_helper" class Cards::Comments::ReactionsControllerTest < ActionDispatch::IntegrationTest setup do - sign_in_as :jz + sign_in_as :david @comment = comments(:logo_agreement_jz) @card = @comment.card end @@ -15,10 +15,19 @@ class Cards::Comments::ReactionsControllerTest < ActionDispatch::IntegrationTest end test "destroy" do - reaction = reactions(:kevin) + reaction = reactions(:david) assert_difference -> { @comment.reactions.count }, -1 do delete card_comment_reaction_path(@comment.card, @comment, reaction, format: :turbo_stream) assert_turbo_stream action: :remove, target: dom_id(reaction) end end + + test "non-owner cannot destroy reaction" do + reaction = reactions(:kevin) + + assert_no_difference -> { @comment.reactions.count } do + delete card_comment_reaction_path(@comment.card, @comment, reaction, format: :turbo_stream) + assert_response :forbidden + end + end end From 4885e220d7a30d572f212524de1f6bd058751c97 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 3 Dec 2025 11:40:39 +0100 Subject: [PATCH 05/82] Install missing tools needed by bin/setup --- bin/setup | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bin/setup b/bin/setup index eee1d9df5..b6da60827 100755 --- a/bin/setup +++ b/bin/setup @@ -31,6 +31,36 @@ if ! command -v gum &>/dev/null; then echo fi +# Install mise if needed +if ! command -v mise &>/dev/null; then + echo + echo "▸ Installing mise" + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm mise + elif command -v brew &>/dev/null; then + brew install mise + else + echo "Please install mise: https://mise.jdx.dev/installing-mise.html#installation-methods" + exit 1 + fi + echo +fi + +# Install gh if needed +if ! command -v gh &>/dev/null; then + echo + echo "▸ Installing GitHub CLI" + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm gh + elif command -v brew &>/dev/null; then + brew install gh + else + echo "Please install GitHub CLI: https://github.com/cli/cli#installation" + exit 1 + fi + echo +fi + step() { local step_name="$1" shift From 0d83e9b90c5c8cba2e76764bddd4cf0f3a351983 Mon Sep 17 00:00:00 2001 From: Javier Valencia Date: Wed, 3 Dec 2025 12:26:39 +0100 Subject: [PATCH 06/82] Refactor with_account and without_account methods Remove variables name for block, not needed from Ruby >= 3.1 --- app/models/current.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/current.rb b/app/models/current.rb index e40fc3156..47f2b6c21 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -12,11 +12,11 @@ class Current < ActiveSupport::CurrentAttributes end end - def with_account(value, &block) - with(account: value, &block) + def with_account(value, &) + with(account: value, &) end - def without_account(&block) - with(account: nil, &block) + def without_account(&) + with(account: nil, &) end end From 184a8279657214655ae60417db0ca86fe62853f0 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Wed, 3 Dec 2025 11:29:42 +0000 Subject: [PATCH 07/82] Show alert message on login when rate limited When the auth-related controllers hit rate limits they set an alert in the flash. But we weren't displaying the flash on the public layout, so those were never seen. Changed to surface the alert. But also change the "Try another code" message to be shake-only instead, to match the current behaviour. --- app/controllers/sessions/magic_links_controller.rb | 2 +- app/views/layouts/public.html.erb | 2 ++ app/views/sessions/magic_links/show.html.erb | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/sessions/magic_links_controller.rb b/app/controllers/sessions/magic_links_controller.rb index dc55782af..eed5f1dac 100644 --- a/app/controllers/sessions/magic_links_controller.rb +++ b/app/controllers/sessions/magic_links_controller.rb @@ -13,7 +13,7 @@ class Sessions::MagicLinksController < ApplicationController start_new_session_for magic_link.identity redirect_to after_sign_in_url(magic_link) else - redirect_to session_magic_link_path, alert: "Try another code." + redirect_to session_magic_link_path, flash: { shake: true } end end diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb index 6072ad16d..253b9bd31 100644 --- a/app/views/layouts/public.html.erb +++ b/app/views/layouts/public.html.erb @@ -15,6 +15,8 @@ <%= yield :header %> + <%= render "layouts/shared/flash" %> +
<%= yield %>
diff --git a/app/views/sessions/magic_links/show.html.erb b/app/views/sessions/magic_links/show.html.erb index f8be106a0..931156ff6 100644 --- a/app/views/sessions/magic_links/show.html.erb +++ b/app/views/sessions/magic_links/show.html.erb @@ -1,6 +1,6 @@ <% @page_title = "Check your email" %> -
"> +
">

<%= @page_title %>

Then enter the verification code included in the email below:

From 0cb65685e51f3d3618ab71768b2426cdba13648c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 3 Dec 2025 13:28:44 +0100 Subject: [PATCH 08/82] Capture key presses inside the column edit form to prevent interferences with column navigation https://app.fizzy.do/5986089/cards/3249 --- app/javascript/controllers/dialog_controller.js | 4 ++++ app/views/boards/show/menu/_column_form.html.erb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index 3df135c12..cdc8ea7cd 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -57,4 +57,8 @@ export default class extends Controller { loadLazyFrames() { Array.from(this.dialogTarget.querySelectorAll("turbo-frame")).forEach(frame => { frame.loading = "eager" }) } + + captureKey(event) { + if (event.key !== "Escape") { event.stopPropagation() } + } } diff --git a/app/views/boards/show/menu/_column_form.html.erb b/app/views/boards/show/menu/_column_form.html.erb index f9eab1c16..80d1be481 100644 --- a/app/views/boards/show/menu/_column_form.html.erb +++ b/app/views/boards/show/menu/_column_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: [board, column], data: { controller: "form", action: "turbo:submit-end->dialog#close turbo:submit-end->form#reset" } do |form| %> +<%= form_with model: [board, column], data: { controller: "form", action: "turbo:submit-end->dialog#close turbo:submit-end->form#reset keydown->dialog#captureKey" } do |form| %> <%= form.text_field :name, class: "input", placeholder: "Name this column", value: column.name, required: true, autocomplete: "off", pattern: ".*\\S.*", title: "Column name cannot be blank", data: { action: "focus->form#select" } %> From 6e9381abb81a1f4918ce2fb08c4612d21df368d8 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 3 Dec 2025 13:27:15 +0100 Subject: [PATCH 09/82] Fix crash in join code redemption race condition --- app/controllers/join_codes_controller.rb | 2 +- app/models/account/join_code.rb | 5 ++- app/models/identity/joinable.rb | 8 ++--- .../controllers/join_codes_controller_test.rb | 4 +-- test/models/account/join_code_test.rb | 14 ++++++-- test/models/identity/joinable_test.rb | 34 +++++++++++++------ 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index 5b0977cd5..971b3c6f8 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -12,7 +12,7 @@ class JoinCodesController < ApplicationController def create identity = Identity.find_or_create_by!(email_address: params.expect(:email_address)) - @join_code.redeem { |account| identity.join(account) } unless identity.member_of?(@join_code.account) + @join_code.redeem_if { |account| identity.join(account) } user = User.active.find_by!(account: @join_code.account, identity: identity) if identity == Current.identity && user.setup? diff --git a/app/models/account/join_code.rb b/app/models/account/join_code.rb index 49fe9af22..82283e4f6 100644 --- a/app/models/account/join_code.rb +++ b/app/models/account/join_code.rb @@ -7,10 +7,9 @@ class Account::JoinCode < ApplicationRecord before_create :generate_code, if: -> { code.blank? } - def redeem + def redeem_if(&block) transaction do - increment!(:usage_count) - yield account if block_given? + increment!(:usage_count) if block.call(account) end end diff --git a/app/models/identity/joinable.rb b/app/models/identity/joinable.rb index 4a6eabcd4..52afb339a 100644 --- a/app/models/identity/joinable.rb +++ b/app/models/identity/joinable.rb @@ -5,11 +5,9 @@ module Identity::Joinable attributes[:name] ||= email_address transaction do - account.users.create!(**attributes, identity: self) + account.users.find_or_create_by!(identity: self) do |user| + user.assign_attributes(attributes) + end.previously_new_record? end end - - def member_of?(account) - account.users.exists?(identity: self) - end end diff --git a/test/controllers/join_codes_controller_test.rb b/test/controllers/join_codes_controller_test.rb index 74613c724..fe8c820d9 100644 --- a/test/controllers/join_codes_controller_test.rb +++ b/test/controllers/join_codes_controller_test.rb @@ -43,7 +43,7 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest identity = identities(:jz) sign_in_as :jz - assert identity.member_of?(@account), "JZ should be a member of 37s for this test" + assert identity.users.exists?(account: @account), "JZ should be a member of 37s for this test" assert identity.users.find_by!(account: @account).setup?, "JZ's user should be setup for this test" assert_no_difference -> { Identity.count } do @@ -59,7 +59,7 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest identity = identities(:mike) sign_in_as :mike - assert_not identity.member_of?(@account), "Mike should not be a member of 37s for this test" + assert_not identity.users.exists?(account: @account), "Mike should not be a member of 37s for this test" assert_no_difference -> { Identity.count } do assert_difference -> { User.count }, 1 do diff --git a/test/models/account/join_code_test.rb b/test/models/account/join_code_test.rb index 4247ce7ec..bebf07ee1 100644 --- a/test/models/account/join_code_test.rb +++ b/test/models/account/join_code_test.rb @@ -10,11 +10,19 @@ class Account::JoinCodeTest < ActiveSupport::TestCase assert_equal 3, parts.count end - test "redeem" do + test "redeem_if increments usage_count when block returns true" do join_code = account_join_codes(:"37s") - assert_difference -> { join_code.reload.usage_count }, 1 do - join_code.redeem + assert_difference -> { join_code.reload.usage_count }, +1 do + join_code.redeem_if { true } + end + end + + test "redeem_if does not increment usage_count when block returns false" do + join_code = account_join_codes(:"37s") + + assert_no_difference -> { join_code.reload.usage_count } do + join_code.redeem_if { false } end end diff --git a/test/models/identity/joinable_test.rb b/test/models/identity/joinable_test.rb index 8a8d40e1c..95ee0e04b 100644 --- a/test/models/identity/joinable_test.rb +++ b/test/models/identity/joinable_test.rb @@ -1,25 +1,37 @@ require "test_helper" class Identity::JoinableTest < ActiveSupport::TestCase - test "join" do + test "join creates a new user and returns true" do identity = identities(:david) - user = identity.join(accounts(:initech)) - assert_kind_of User, user - assert_equal accounts(:initech), user.account - assert_equal identity.email_address, user.name + assert_difference -> { User.count }, 1 do + result = identity.join(accounts(:initech)) + assert result, "join should return true when creating a new user" + end + user = identity.users.find_by!(account: accounts(:initech)) + assert_equal identity.email_address, user.name + end + + test "join with custom attributes" do identity = identities(:mike) - user = identity.join(accounts("37s"), name: "Mike") - assert_kind_of User, user - assert_equal accounts("37s"), user.account + result = identity.join(accounts("37s"), name: "Mike") + assert result + + user = identity.users.find_by!(account: accounts("37s")) assert_equal "Mike", user.name end - test "member_of?" do + test "join returns false if user already exists" do identity = identities(:david) - assert identity.member_of?(accounts("37s")) - assert_not identity.member_of?(accounts(:initech)) + account = accounts("37s") + + assert identity.users.exists?(account: account), "David should already be a member of 37s" + + assert_no_difference -> { User.count } do + result = identity.join(account) + assert_not result, "join should return false when user already exists" + end end end From 3832b6230f49a43ec332b3315a7e8fb5058dea44 Mon Sep 17 00:00:00 2001 From: Vladimir Dementyev Date: Wed, 3 Dec 2025 16:40:44 +0300 Subject: [PATCH 10/82] + connection_test.rb --- .../application_cable/connection_test.rb | 37 +++++++++++++++---- test/fixtures/sessions.yml | 3 ++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb index 6340bf9c0..5f661b0e0 100644 --- a/test/channels/application_cable/connection_test.rb +++ b/test/channels/application_cable/connection_test.rb @@ -2,12 +2,35 @@ require "test_helper" module ApplicationCable class ConnectionTest < ActionCable::Connection::TestCase - # test "connects with cookies" do - # cookies.signed[:user_id] = 42 - # - # connect - # - # assert_equal connection.user_id, "42" - # end + setup do + # Use non-37s account to assess that Current.account is set correctly + @account = accounts(:initech) + @session = sessions(:mike) + end + + test "connects with valid session and account info" do + cookies.signed[:session_token] = @session.signed_id + + connect "/cable", env: { "fizzy.external_account_id" => @account.external_account_id } + + assert_equal users(:mike), connection.current_user + assert_equal @account, Current.account + end + + test "rejects with invalid session token" do + cookies.signed[:session_token] = "invalid-session-id" + + assert_reject_connection do + connect "/cable", env: { "fizzy.external_account_id" => @account.external_account_id } + end + end + + test "rejects when account does not exist" do + cookies.signed[:session_token] = @session.signed_id + + assert_reject_connection do + connect "/cable", env: { "fizzy.external_account_id" => -1 } + end + end end end diff --git a/test/fixtures/sessions.yml b/test/fixtures/sessions.yml index a062f53fd..56fc614a5 100644 --- a/test/fixtures/sessions.yml +++ b/test/fixtures/sessions.yml @@ -9,3 +9,6 @@ jz: jason: identity: jason + +mike: + identity: mike From 737d92781b3b1fa5c75a7531c324ec89b40a032f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 3 Dec 2025 14:57:59 +0100 Subject: [PATCH 11/82] We now use GH actions instead of local CI since this project is public --- bin/setup | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/setup b/bin/setup index b6da60827..2af87a4e7 100755 --- a/bin/setup +++ b/bin/setup @@ -121,9 +121,6 @@ if which pacman >/dev/null 2>&1; then fi fi -# Ensure gh-signoff is installed and up to date -step "Set up gh-signoff" bash -c "gh extension install basecamp/gh-signoff || gh extension upgrade basecamp/gh-signoff" - bundle config set --local auto_install true step "Installing RubyGems" bundle install From 7e95322776110238fba68a74109ee5c61f5d2ee5 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Wed, 3 Dec 2025 11:20:02 -0300 Subject: [PATCH 12/82] Prefer find_by! over where + first! --- app/models/account.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/account.rb b/app/models/account.rb index f74660cbb..50f3bf3ee 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -35,7 +35,7 @@ class Account < ApplicationRecord end def system_user - users.where(role: :system).first! + users.find_by!(role: :system) end private From 13db38464863972c141816c89b07544b096e8b4b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 3 Dec 2025 14:11:46 +0000 Subject: [PATCH 13/82] Auto default for UUID primary keys Patch load_schmema! to set the default value for UUID primary keys. This removes the need to patch ApplicationRecord + Rails models individually. It also means we no longer need to patch the default in for the integer primary key in Search::Record::SQLite. --- app/models/application_record.rb | 2 - app/models/search/record/sqlite.rb | 2 - config/initializers/uuid_framework_models.rb | 6 +-- config/initializers/uuid_primary_keys.rb | 43 +++++++++++++++++++- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 45e9c2f21..4ed46f2c3 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,6 +2,4 @@ class ApplicationRecord < ActiveRecord::Base primary_abstract_class configure_replica_connections - - attribute :id, :uuid, default: -> { ActiveRecord::Type::Uuid.generate } end diff --git a/app/models/search/record/sqlite.rb b/app/models/search/record/sqlite.rb index c3da9131a..ae0d34281 100644 --- a/app/models/search/record/sqlite.rb +++ b/app/models/search/record/sqlite.rb @@ -2,8 +2,6 @@ module Search::Record::SQLite extend ActiveSupport::Concern included do - # Override default UUID id attribute, as FTS5 uses rowid integer primary key - attribute :id, :integer, default: nil attribute :result_title, :string attribute :result_content, :string diff --git a/config/initializers/uuid_framework_models.rb b/config/initializers/uuid_framework_models.rb index c0e900176..3345dd97b 100644 --- a/config/initializers/uuid_framework_models.rb +++ b/config/initializers/uuid_framework_models.rb @@ -1,14 +1,10 @@ -# Inject UUID primary key support into Rails framework models +# Inject account associations into Rails framework models Rails.application.config.to_prepare do - ActionText::RichText.attribute :id, :uuid, default: -> { ActiveRecord::Type::Uuid.generate } ActionText::RichText.belongs_to :account, default: -> { record.account } - ActiveStorage::Attachment.attribute :id, :uuid, default: -> { ActiveRecord::Type::Uuid.generate } ActiveStorage::Attachment.belongs_to :account, default: -> { record.account } - ActiveStorage::Blob.attribute :id, :uuid, default: -> { ActiveRecord::Type::Uuid.generate } ActiveStorage::Blob.belongs_to :account, default: -> { Current.account } - ActiveStorage::VariantRecord.attribute :id, :uuid, default: -> { ActiveRecord::Type::Uuid.generate } ActiveStorage::VariantRecord.belongs_to :account, default: -> { blob.account } end diff --git a/config/initializers/uuid_primary_keys.rb b/config/initializers/uuid_primary_keys.rb index 56ff7f3af..01b54eab5 100644 --- a/config/initializers/uuid_primary_keys.rb +++ b/config/initializers/uuid_primary_keys.rb @@ -1,4 +1,30 @@ -# Automatically use UUID type for all binary(16) columns +# Automatically use UUID type for all binary(16) columns and generate defaults + +module UuidPrimaryKeyDefault + def load_schema! + define_uuid_primary_key_pending_default + super + end + + private + def define_uuid_primary_key_pending_default + if uuid_primary_key? + pending_attribute_modifications << PendingUuidDefault.new(primary_key) + end + rescue ActiveRecord::StatementInvalid + # Table doesn't exist yet + end + + def uuid_primary_key? + table_name && primary_key && schema_cache.columns_hash(table_name)[primary_key]&.type == :uuid + end + + PendingUuidDefault = Struct.new(:name) do + def apply_to(attribute_set) + attribute_set[name] = attribute_set[name].with_user_default(-> { ActiveRecord::Type::Uuid.generate }) + end + end +end module MysqlUuidAdapter extend ActiveSupport::Concern @@ -12,6 +38,20 @@ module MysqlUuidAdapter end end + # Override fetch_type_metadata to preserve UUID type and limit + def fetch_type_metadata(sql_type, extra = "") + if sql_type == "binary(16)" + simple_type = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new( + sql_type: sql_type, + type: :uuid, + limit: 16 + ) + ActiveRecord::ConnectionAdapters::MySQL::TypeMetadata.new(simple_type, extra: extra) + else + super + end + end + class_methods do def native_database_types @native_database_types_with_uuid ||= super.merge(uuid: { name: "binary", limit: 16 }) @@ -69,6 +109,7 @@ module TableDefinitionUuidSupport end ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.singleton_class.prepend(UuidPrimaryKeyDefault) ActiveRecord::ConnectionAdapters::TableDefinition.prepend(TableDefinitionUuidSupport) end From 84213265e7ca3481409583747da73d4d04915931 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 3 Dec 2025 15:37:27 +0100 Subject: [PATCH 14/82] Render friendly error message when using an invalid token --- .../email_addresses/confirmations_controller.rb | 12 +++++++----- app/models/user/email_address_changeable.rb | 14 +++++--------- .../confirmations/invalid_token.html.erb | 16 ++++++++++++++++ .../confirmations_controller_test.rb | 11 ++++++----- .../models/user/email_address_changeable_test.rb | 15 ++++++++------- 5 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 app/views/users/email_addresses/confirmations/invalid_token.html.erb diff --git a/app/controllers/users/email_addresses/confirmations_controller.rb b/app/controllers/users/email_addresses/confirmations_controller.rb index dad998671..b5fd334ea 100644 --- a/app/controllers/users/email_addresses/confirmations_controller.rb +++ b/app/controllers/users/email_addresses/confirmations_controller.rb @@ -8,12 +8,14 @@ class Users::EmailAddresses::ConfirmationsController < ApplicationController end def create - user = @user.change_email_address_using_token(token) + if @user.change_email_address_using_token(token) + terminate_session if Current.session + start_new_session_for @user.identity - terminate_session if Current.session - start_new_session_for user.identity - - redirect_to edit_user_url(script_name: user.account.slug, id: user) + redirect_to edit_user_url(script_name: @user.account.slug, id: @user) + else + render :invalid_token, status: :unprocessable_entity + end end private diff --git a/app/models/user/email_address_changeable.rb b/app/models/user/email_address_changeable.rb index ce18d46d0..2c8de7217 100644 --- a/app/models/user/email_address_changeable.rb +++ b/app/models/user/email_address_changeable.rb @@ -7,14 +7,12 @@ module User::EmailAddressChangeable def change_email_address_using_token(token) parsed_token = SignedGlobalID.parse(token, for: EMAIL_CHANGE_TOKEN_PURPOSE) - if parsed_token.nil? - raise ArgumentError, "The token is invalid" - elsif parsed_token.find != self - raise ArgumentError, "The token was generated for a different user" - elsif identity.email_address != parsed_token.params.fetch("old_email_address") - raise ArgumentError, "The token was generated for a different email address" + old_email_address = parsed_token&.params&.fetch("old_email_address") + new_email_address = parsed_token&.params&.fetch("new_email_address") + + if parsed_token.nil? || parsed_token.find != self || identity.email_address != old_email_address + false else - new_email_address = parsed_token.params.fetch("new_email_address") change_email_address(new_email_address) end end @@ -37,8 +35,6 @@ module User::EmailAddressChangeable new_identity = Identity.find_or_create_by!(email_address: new_email_address) update!(identity: new_identity) end - - self end private diff --git a/app/views/users/email_addresses/confirmations/invalid_token.html.erb b/app/views/users/email_addresses/confirmations/invalid_token.html.erb new file mode 100644 index 000000000..13e66c97d --- /dev/null +++ b/app/views/users/email_addresses/confirmations/invalid_token.html.erb @@ -0,0 +1,16 @@ +<% @page_title = "Confirm email change" %> + +
+
+

+ <%= @page_title %> +

+

Something went wrong.

+
+ +

+ It looks like the email confirmation link is no longer valid. + Try changing your email address again from <%= link_to "your profile", edit_user_url(script_name: @user.account.slug, id: @user) %>. + If that doesn't help, please <%= mail_to "send us an email", "support@fizzy.do" %>. +

+
diff --git a/test/controllers/users/email_addresses/confirmations_controller_test.rb b/test/controllers/users/email_addresses/confirmations_controller_test.rb index b16b6ea7c..a68cfd175 100644 --- a/test/controllers/users/email_addresses/confirmations_controller_test.rb +++ b/test/controllers/users/email_addresses/confirmations_controller_test.rb @@ -3,6 +3,7 @@ require "test_helper" class Users::EmailAddresses::ConfirmationsControllerTest < ActionDispatch::IntegrationTest setup do @user = users(:david) + @old_email = @user.identity.email_address @new_email = "newemail@example.com" @token = @user.send(:generate_email_address_change_token, to: @new_email) end @@ -13,8 +14,6 @@ class Users::EmailAddresses::ConfirmationsControllerTest < ActionDispatch::Integ end test "create" do - old_email = @user.identity.email_address - post user_email_address_confirmation_path(user_id: @user.id, email_address_token: @token, script_name: @user.account.slug) assert_equal @new_email, @user.reload.identity.email_address @@ -22,8 +21,10 @@ class Users::EmailAddresses::ConfirmationsControllerTest < ActionDispatch::Integ end test "create with invalid token" do - assert_raises(ArgumentError) do - post user_email_address_confirmation_path(user_id: @user.id, email_address_token: "invalid", script_name: @user.account.slug) - end + post user_email_address_confirmation_path(user_id: @user.id, email_address_token: "invalid", script_name: @user.account.slug) + + assert_equal @user.identity.email_address, @old_email + assert_response :unprocessable_entity + assert_match /Something went wrong/, response.body end end diff --git a/test/models/user/email_address_changeable_test.rb b/test/models/user/email_address_changeable_test.rb index 5e1158cf3..951170297 100644 --- a/test/models/user/email_address_changeable_test.rb +++ b/test/models/user/email_address_changeable_test.rb @@ -7,6 +7,7 @@ class User::EmailAddressChangeableTest < ActiveSupport::TestCase @identity = identities(:kevin) @user = @identity.users.find_by!(account: accounts("37s")) @new_email = "newart@example.com" + @old_email = @identity.email_address end test "send_email_address_change_confirmation" do @@ -42,15 +43,15 @@ class User::EmailAddressChangeableTest < ActiveSupport::TestCase end test "change_email_address_using_token with invalid token" do - assert_raises(ArgumentError, match: /invalid/) do - @user.change_email_address_using_token("invalid_token") - end + assert_not @user.change_email_address_using_token("invalid_token") + assert_equal @old_email, @user.reload.identity.email_address token = @user.send(:generate_email_address_change_token, to: @new_email) - @identity.update!(email_address: "different@example.com") + old_email = "#{SecureRandom.hex(16)}@example.com" + @identity.update!(email_address: old_email) + @user.reload - assert_raises(ArgumentError, match: /different email address/) do - @user.change_email_address_using_token(token) - end + assert_not @user.change_email_address_using_token(token) + assert_equal old_email, @user.reload.identity.email_address end end From 92b7c4a86ce3a4f9af3da320e53ec702d03d22a3 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 3 Dec 2025 15:44:20 +0100 Subject: [PATCH 15/82] Update copy --- .../confirmations/invalid_token.html.erb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/users/email_addresses/confirmations/invalid_token.html.erb b/app/views/users/email_addresses/confirmations/invalid_token.html.erb index 13e66c97d..fb62fbda7 100644 --- a/app/views/users/email_addresses/confirmations/invalid_token.html.erb +++ b/app/views/users/email_addresses/confirmations/invalid_token.html.erb @@ -8,9 +8,12 @@

Something went wrong.

-

- It looks like the email confirmation link is no longer valid. - Try changing your email address again from <%= link_to "your profile", edit_user_url(script_name: @user.account.slug, id: @user) %>. - If that doesn't help, please <%= mail_to "send us an email", "support@fizzy.do" %>. +

+ It looks like this email confirmation link is no longer valid. They last only 30 minues. + Try changing your email address again from <%= link_to "your profile", edit_user_url(script_name: @user.account.slug, id: @user) %>. +

+ +

+ If that doesn't help, please <%= mail_to "support@fizzy.do", "send us an email" %>.

From 59d722950022716abba7cbc530130df4a19c3f99 Mon Sep 17 00:00:00 2001 From: Karl Entwistle Date: Wed, 3 Dec 2025 16:35:05 +0000 Subject: [PATCH 16/82] Move Eventable concern to app/models/concerns Aligns with Rails conventions for organizing concerns in a dedicated concerns directory alongside existing concerns like Attachments, Mentions, Searchable, etc. --- app/models/{ => concerns}/eventable.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/models/{ => concerns}/eventable.rb (100%) diff --git a/app/models/eventable.rb b/app/models/concerns/eventable.rb similarity index 100% rename from app/models/eventable.rb rename to app/models/concerns/eventable.rb From d62a608772bcd22ea7dcc46f44670366c1731df0 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 3 Dec 2025 11:02:02 -0600 Subject: [PATCH 17/82] Repair margins for attachments --- app/assets/stylesheets/comments.css | 12 +++++++----- app/assets/stylesheets/rich-text-content.css | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index b58156f36..80968184e 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -51,12 +51,14 @@ padding-inline-end: var(--reaction-size); text-align: start; - :first-child { - margin-block-start: 0; - } + .action-text-content { + > action-text-attachment:first-child figure { + margin-block-start: 0.5ch; + } - :last-child { - margin-block-end: 0; + > :last-child { + margin-block-end: 0; + } } } diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 38c7722e2..2d465c935 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -62,7 +62,7 @@ /* Links should hug media contained within */ a:has(img), a:has(video) { - display: inline-block; + inline-size: fit-content; } /* Avoid extra space due to empty paragraphs */ @@ -263,7 +263,7 @@ display: flex; flex-wrap: wrap; justify-content: center; - margin-block-start: var(--block-space-half); + margin-block-start: 0.5ch; } } From c3172c66eeab75a5ff3f56814bd4ccf1cc866fae Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 3 Dec 2025 11:16:37 -0600 Subject: [PATCH 18/82] Target tooltips instead of specific buttons --- app/assets/stylesheets/filters.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index bc0ebdf98..22c40342c 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -18,7 +18,7 @@ --input-background: var(--color-canvas); } - &:has(.filter-toggle:hover) { + &:has([data-controller~="tooltip"]:hover) { z-index: calc(var(--z-nav) + 1); } } From 30a2149115cd231f614c961ea28c27d3bdb72354 Mon Sep 17 00:00:00 2001 From: Brian Bailey Date: Wed, 3 Dec 2025 12:20:37 -0500 Subject: [PATCH 19/82] Update seeder.rb to fix a few minor things in the onboarding content --- app/models/account/seeder.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 354674ddc..4a349b939 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -29,12 +29,12 @@ class Account::Seeder # Cards playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML -

There’s a whole lot more you can do in Fizzy. In the video below 37signals founder and CEO, Jason Fried, will walk you through the basics in just 8 minutes.

+

There’s a whole lot more you can do in Fizzy. In the video below, 37signals founder and CEO, Jason Fried, will walk you through the basics in just 17 minutes.

HTML playground.cards.create! creator: creator, title: "Now, grab the invite link to invite someone else", status: "published", description: <<~HTML -

Open Fizzy menu, select “+ Add people”, then copy the invite link. You can give this link to someone else so they can make an login for themselves in your account.

+

Open the Fizzy menu, select “+ Add people”, then copy the invite link. You can give this link to someone else so they can make a login for themselves in your account.

HTML @@ -43,7 +43,7 @@ class Account::Seeder HTML - playground.cards.create! creator: creator, title: "Now,check out all cards assigned to you", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Now, check out all cards assigned to you", status: "published", description: <<~HTML

Pull down the Fizzy menu at the top of the screen, and select “Assigned to me” or just hit “2” on your keyboard any time.

HTML @@ -54,7 +54,7 @@ class Account::Seeder HTML playground.cards.create! creator: creator, title: "Next, assign this card to yourself", status: "published", description: <<~HTML -

Click the little head with the + next to it, pick yourself.

+

Click the little head with the + next to it, then pick yourself.

HTML From a6c5cc19b7edcacd8b1545b486333bc29e2c4247 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 3 Dec 2025 11:21:13 -0600 Subject: [PATCH 20/82] Copy and layout edits for the new invalid token screen --- .../confirmations/invalid_token.html.erb | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/app/views/users/email_addresses/confirmations/invalid_token.html.erb b/app/views/users/email_addresses/confirmations/invalid_token.html.erb index fb62fbda7..ec3950efb 100644 --- a/app/views/users/email_addresses/confirmations/invalid_token.html.erb +++ b/app/views/users/email_addresses/confirmations/invalid_token.html.erb @@ -1,19 +1,16 @@ -<% @page_title = "Confirm email change" %> +<% @page_title = "Link expired" %> -
-
-

- <%= @page_title %> -

-

Something went wrong.

-
- -

- It looks like this email confirmation link is no longer valid. They last only 30 minues. - Try changing your email address again from <%= link_to "your profile", edit_user_url(script_name: @user.account.slug, id: @user) %>. +

+

+ <%= @page_title %> +

+

+ That email confirmation link is no longer valid—they expire after 30 minues. You’ll have to try again.

-

- If that doesn't help, please <%= mail_to "support@fizzy.do", "send us an email" %>. + <%= link_to "Change my email address", new_user_email_address_path(user_id: @user, script_name: @user.account.slug), class: "btn btn--link center" %> + +

+ If you get stuck, <%= mail_to "support@fizzy.do", "send us an email" %> and we’ll get you back on track.

From df04fb92504fb194a09d423715d006e0e4078760 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 3 Dec 2025 11:21:34 -0600 Subject: [PATCH 21/82] Adjust for layout consistency and tighten up some copy --- app/views/users/email_addresses/confirmations/show.html.erb | 4 ++-- app/views/users/email_addresses/create.html.erb | 4 ++-- app/views/users/email_addresses/new.html.erb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/users/email_addresses/confirmations/show.html.erb b/app/views/users/email_addresses/confirmations/show.html.erb index ed6e24f72..15347e887 100644 --- a/app/views/users/email_addresses/confirmations/show.html.erb +++ b/app/views/users/email_addresses/confirmations/show.html.erb @@ -1,11 +1,11 @@ <% @page_title = "Confirm email change" %> -
+

<%= @page_title %>

-

Just a sec while we confirm your new email address.

+

Just a sec while we confirm your new email address.

<%= form_with url: user_email_address_confirmation_path(user_id: @user.id), method: :post, data: { controller: "form auto-submit" } do |form| %> diff --git a/app/views/users/email_addresses/create.html.erb b/app/views/users/email_addresses/create.html.erb index 0ed95d7cc..00b660457 100644 --- a/app/views/users/email_addresses/create.html.erb +++ b/app/views/users/email_addresses/create.html.erb @@ -6,10 +6,10 @@
<% end %> -
+

Check your email

We just sent an email to <%= params[:email_address] %>

-

Hit the link in the email to confirm this is the email address you want to use with Fizzy going forward.

+

Hit the link in the email to confirm this is the email address you want to use with Fizzy going forward.

<%= link_to "Done", edit_user_path(@user, script_name: @user.account.slug), class: "btn btn--link center" %>
diff --git a/app/views/users/email_addresses/new.html.erb b/app/views/users/email_addresses/new.html.erb index 397e62300..589dca937 100644 --- a/app/views/users/email_addresses/new.html.erb +++ b/app/views/users/email_addresses/new.html.erb @@ -6,7 +6,7 @@
<% end %> -
+

<%= @page_title %>

@@ -18,7 +18,7 @@
-

Enter your new email address, then hit the link to confirm it

+

Enter your new email address, then check your email to confirm the change.