From 89973d97d4468dd4b079bc4c44150d6bcc1c2aca Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 12 Mar 2026 07:45:18 +0900 Subject: [PATCH 1/2] Add ActionCable connection identification Co-Authored-By: Claude Opus 4.6 --- app/channels/application_cable/channel.rb | 7 +++++ app/channels/application_cable/connection.rb | 20 ++++++++++++++ app/views/display/shops/show.html.erb | 1 + .../application_cable/connection_test.rb | 26 ++++++++++++++----- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index d672697..c0cf330 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,4 +1,11 @@ module ApplicationCable class Channel < ActionCable::Channel::Base + # All current channels are public (Turbo::StreamsChannel for display pages). + # If an authenticated channel is added in the future, reject unauthorized + # connections in that channel's #subscribed method: + # + # def subscribed + # reject unless connection.current_shopkeeper + # end end end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442..5ac4e3e 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,24 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_shopkeeper, :current_account + + def connect + self.current_shopkeeper = find_shopkeeper + self.current_account = find_account + end + + private + + def find_shopkeeper + env["warden"]&.user(:shopkeeper) + end + + # Display pages are public — anonymous connections are allowed. + # Shopkeeper auth is header-based (devise_token_auth), so most + # WebSocket connections will be anonymous. If an authenticated-only + # channel is added in the future, reject in that channel's #subscribed. + def find_account + current_shopkeeper&.accounts&.order(created_at: :asc)&.first + end end end diff --git a/app/views/display/shops/show.html.erb b/app/views/display/shops/show.html.erb index 2ce396e..2a5a0db 100644 --- a/app/views/display/shops/show.html.erb +++ b/app/views/display/shops/show.html.erb @@ -1,3 +1,4 @@ +<%# These streams are public by design — display pages are unauthenticated %> <%= turbo_stream_from @shop, :tb_stream_full_reload_entire_page %> <%= turbo_stream_from @shop, :tb_stream_update_item_tags %> diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb index 800405f..2d7ae4d 100644 --- a/test/channels/application_cable/connection_test.rb +++ b/test/channels/application_cable/connection_test.rb @@ -1,11 +1,23 @@ require "test_helper" class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase - # test "connects with cookies" do - # cookies.signed[:user_id] = 42 - # - # connect - # - # assert_equal connection.user_id, "42" - # end + test "anonymous connection succeeds with nil shopkeeper and account" do + connect + + assert_nil connection.current_shopkeeper + assert_nil connection.current_account + end + + test "authenticated connection identifies shopkeeper and account" do + shopkeeper = shopkeepers(:one) + account = shopkeeper.create_default_account + warden = Minitest::Mock.new + warden.expect(:user, shopkeeper, [:shopkeeper]) + + connect env: {"warden" => warden} + + assert_equal shopkeeper, connection.current_shopkeeper + assert_equal account, connection.current_account + warden.verify + end end From 8544a62bf6d1feaf84e2d64a549d2c2866b8f84d Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 12 Mar 2026 08:43:48 +0900 Subject: [PATCH 2/2] Fix ActionCable find_account to use request path Co-Authored-By: Claude Opus 4.6 --- app/channels/application_cable/connection.rb | 11 ++++++----- test/channels/application_cable/connection_test.rb | 14 +++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 5ac4e3e..3ea2751 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -13,12 +13,13 @@ def find_shopkeeper env["warden"]&.user(:shopkeeper) end - # Display pages are public — anonymous connections are allowed. - # Shopkeeper auth is header-based (devise_token_auth), so most - # WebSocket connections will be anonymous. If an authenticated-only - # channel is added in the future, reject in that channel's #subscribed. + # Extract the account UUID from the WebSocket upgrade request path, + # matching how AccountMiddleware sets the current account for HTTP + # requests. Display page URLs (display/shops/...) don't include an + # account UUID, so this returns nil for public connections. def find_account - current_shopkeeper&.accounts&.order(created_at: :asc)&.first + _, account_id, = request.path.split("/", 3) + Account.find_by(id: account_id) if AccountMiddleware::UUID_MATCHER.match?(account_id) end end end diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb index 2d7ae4d..8e31519 100644 --- a/test/channels/application_cable/connection_test.rb +++ b/test/channels/application_cable/connection_test.rb @@ -14,10 +14,22 @@ class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase warden = Minitest::Mock.new warden.expect(:user, shopkeeper, [:shopkeeper]) - connect env: {"warden" => warden} + connect "/#{account.id}/cable", env: {"warden" => warden} assert_equal shopkeeper, connection.current_shopkeeper assert_equal account, connection.current_account warden.verify end + + test "connection without account UUID in path has nil account" do + shopkeeper = shopkeepers(:one) + warden = Minitest::Mock.new + warden.expect(:user, shopkeeper, [:shopkeeper]) + + connect "/cable", env: {"warden" => warden} + + assert_equal shopkeeper, connection.current_shopkeeper + assert_nil connection.current_account + warden.verify + end end