From aada38014798c88669cc187c65c92a4ffd258f01 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 11 Jun 2025 17:46:02 -0400 Subject: [PATCH 01/18] Add the signal_id gem - add libmysqlclient to the container image - configure the signal id database - YAML permitted classes setup for 37id tests - setup rw splitting so integration tests pass --- Dockerfile | 5 ++- Gemfile | 8 ++++ Gemfile.lock | 68 ++++++++++++++++++++++++++++++++ config/initializers/signal_id.rb | 10 +++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 config/initializers/signal_id.rb diff --git a/Dockerfile b/Dockerfile index e2308f371..25ee37e68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,8 @@ FROM base AS build # Install packages needed to build gems RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git libvips pkg-config libyaml-dev + apt-get install -y --no-install-recommends -y build-essential git libvips pkg-config libyaml-dev default-libmysqlclient-dev && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives # Install application gems COPY Gemfile Gemfile.lock .ruby-version ./ @@ -47,7 +48,7 @@ FROM base # Install packages needed for deployment RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools default-libmysqlclient-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Copy built artifacts: gems, application diff --git a/Gemfile b/Gemfile index 8c43ff533..cc84e2398 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,14 @@ gem "image_processing", "~> 1.14" gem "platform_agent" gem "aws-sdk-s3", require: false +# 37id and Queenbee integration +gem "signal_id", bc: "signal_id", branch: "rails4" +gem "mysql2" # needed by signal_id +gem "queuety", bc: "queuety", branch: "rails4" # needed by signal_id +gem "service_concurrency_prevention", bc: "service_concurrency_prevention" # needed by queuety +gem "portfolio", ">= 4.6", bc: "portfolio" # needed by signal_id +gem "file_repository", "~> 1.4.5", bc: "file_repository" # needed by portfolio + # Telemetry, logging, and operations gem "mission_control-jobs" gem "sentry-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index ab120f738..780151b61 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,6 +15,12 @@ GIT railties (>= 8.1.alpha) zeitwerk +GIT + remote: https://github.com/basecamp/file_repository + revision: 17e90fc35ccb9f0a0dae1ed8623f01386696002d + specs: + file_repository (1.4.5) + GIT remote: https://github.com/basecamp/kamal revision: 344e2d7995e0134aa52f195a984d8d5e24e31d94 @@ -32,6 +38,23 @@ GIT thor (~> 1.3) zeitwerk (>= 2.6.18, < 3.0) +GIT + remote: https://github.com/basecamp/portfolio + revision: 26dd0ccbe4aad5a5c539f90c00e82b6175443636 + specs: + portfolio (5.2.0) + activesupport + file_repository (>= 1.4.4) + +GIT + remote: https://github.com/basecamp/queuety + revision: bf7e2a552fb674533d3e092f06dbf9b5d1ebc753 + branch: rails4 + specs: + queuety (4.0.2) + activerecord + service_concurrency_prevention + GIT remote: https://github.com/basecamp/rails-structured-logging revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6 @@ -40,6 +63,29 @@ GIT json rails (>= 6.0.0) +GIT + remote: https://github.com/basecamp/service_concurrency_prevention + revision: 320da99431718615ed86f8723e8b9d3259c602a3 + specs: + service_concurrency_prevention (1.1.1) + activesupport + +GIT + remote: https://github.com/basecamp/signal_id + revision: 7090b0d518b6656d8c8792da8301146da40f3eae + branch: rails4 + specs: + signal_id (4.3.2) + bcrypt-ruby (>= 3.0.0) + google-id-token (>= 1.4.0) + mocha + multi_json + portfolio (>= 4.0) + pwned (~> 2.0) + queuety (>= 3.0) + rails (>= 4.0.2) + rails-controller-testing + GIT remote: https://github.com/crmne/ruby_llm.git revision: a35aa111a0bb36b7f0363b1f1f2a25e3d5378fd3 @@ -182,6 +228,8 @@ GEM aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) bcrypt (3.1.20) + bcrypt-ruby (3.1.5) + bcrypt (>= 3.1.3) bcrypt_pbkdf (1.1.1) benchmark (0.4.1) bigdecimal (3.2.2) @@ -243,6 +291,8 @@ GEM addressable (>= 2.5.0) globalid (1.2.1) activesupport (>= 6.1) + google-id-token (1.4.2) + jwt (>= 1) hashdiff (1.2.0) i18n (1.14.7) concurrent-ruby (~> 1.0) @@ -263,6 +313,8 @@ GEM activesupport (>= 5.0.0) jmespath (1.6.2) json (2.12.2) + jwt (2.10.1) + base64 language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) @@ -292,8 +344,12 @@ GEM railties (>= 7.1) stimulus-rails turbo-rails + mocha (2.7.1) + ruby2_keywords (>= 0.0.5) msgpack (1.8.0) + multi_json (1.15.0) multipart-post (2.4.1) + mysql2 (0.5.6) net-http (0.6.0) uri net-imap (0.5.8) @@ -343,6 +399,7 @@ GEM public_suffix (6.0.2) puma (6.6.0) nio4r (~> 2.0) + pwned (2.4.1) raabro (1.4.0) racc (1.8.1) rack (3.1.16) @@ -355,6 +412,10 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -410,6 +471,7 @@ GEM ruby-vips (2.2.3) ffi (~> 1.12) logger + ruby2_keywords (0.0.5) rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.33.0) @@ -513,15 +575,19 @@ DEPENDENCIES bundler-audit capybara debug + file_repository (~> 1.4.5)! geared_pagination (~> 1.2) image_processing (~> 1.14) importmap-rails jbuilder kamal! mission_control-jobs + mysql2 platform_agent + portfolio (>= 4.6)! propshaft puma (>= 5.0) + queuety! rails! rails_structured_logging! redcarpet @@ -532,6 +598,8 @@ DEPENDENCIES selenium-webdriver sentry-rails sentry-ruby + service_concurrency_prevention! + signal_id! solid_cable (>= 3.0) solid_cache (~> 1.0) solid_queue (~> 1.1) diff --git a/config/initializers/signal_id.rb b/config/initializers/signal_id.rb new file mode 100644 index 000000000..cf88348c0 --- /dev/null +++ b/config/initializers/signal_id.rb @@ -0,0 +1,10 @@ +require "signal_id" + +Rails.application.config.to_prepare do + SignalId::Database.load_configuration SignalId::Database.default_configuration + SignalId::Database.enable_rw_splitting! +end + +Rails.application.config.after_initialize do + ActiveRecord.yaml_column_permitted_classes << SignalId::PersonName +end From 192c6036bc79fec1ad15696f03042b211bb47efa Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 12 Jun 2025 12:22:58 -0400 Subject: [PATCH 02/18] Account belongs to Signal::Account --- app/models/account.rb | 2 +- app/models/account/signal_account.rb | 8 +++++ ...50611203906_add_queenbee_id_to_accounts.rb | 6 ++++ db/schema.rb | 4 ++- db/schema_cache.yml | 30 +++++++++++++++++-- test/fixtures/accounts.yml | 1 + test/models/account/signal_account_test.rb | 13 ++++++++ test/models/account_test.rb | 5 +--- test/test_helper.rb | 2 ++ 9 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 app/models/account/signal_account.rb create mode 100644 db/migrate/20250611203906_add_queenbee_id_to_accounts.rb create mode 100644 test/models/account/signal_account_test.rb diff --git a/app/models/account.rb b/app/models/account.rb index afd022441..c562043b2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,5 +1,5 @@ class Account < ApplicationRecord - include Entropic, Joinable + include Entropic, Joinable, SignalAccount has_many_attached :uploads end diff --git a/app/models/account/signal_account.rb b/app/models/account/signal_account.rb new file mode 100644 index 000000000..99af103bb --- /dev/null +++ b/app/models/account/signal_account.rb @@ -0,0 +1,8 @@ +module Account::SignalAccount + extend ActiveSupport::Concern + + included do + # TODO: remove the "optional: true" once we've populated the accounts properly + belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true + end +end diff --git a/db/migrate/20250611203906_add_queenbee_id_to_accounts.rb b/db/migrate/20250611203906_add_queenbee_id_to_accounts.rb new file mode 100644 index 000000000..8d1d55ad4 --- /dev/null +++ b/db/migrate/20250611203906_add_queenbee_id_to_accounts.rb @@ -0,0 +1,6 @@ +class AddQueenbeeIdToAccounts < ActiveRecord::Migration[8.1] + def change + add_column :accounts, :queenbee_id, :integer # I solemnly swear I will add a "NOT NULL" constraint once it's populated + add_index :accounts, :queenbee_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index dff0ccb02..28b66be58 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do +ActiveRecord::Schema[8.1].define(version: 2025_06_11_203906) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -26,7 +26,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do t.datetime "created_at", null: false t.string "join_code" t.string "name", null: false + t.integer "queenbee_id" t.datetime "updated_at", null: false + t.index ["queenbee_id"], name: "index_accounts_on_queenbee_id", unique: true end create_table "action_text_rich_texts", force: :cascade do |t| diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 89fca8331..751801e34 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -113,6 +113,16 @@ columns: default_function: collation: comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: queenbee_id + cast_type: *1 + sql_type_metadata: *2 + 'null': true + default: + default_function: + collation: + comment: - *9 action_text_rich_texts: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -1163,7 +1173,23 @@ indexes: nulls_not_distinct: comment: valid: true - accounts: [] + accounts: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: accounts + name: index_accounts_on_queenbee_id + unique: true + columns: + - queenbee_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true action_text_rich_texts: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: action_text_rich_texts @@ -2301,4 +2327,4 @@ indexes: comment: valid: true workflows: [] -version: 20250609102553 +version: 20250611203906 diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml index 587f05a44..d394aedc3 100644 --- a/test/fixtures/accounts.yml +++ b/test/fixtures/accounts.yml @@ -1,3 +1,4 @@ 37s: name: 37signals join_code: "ejpP-THlQ-Cc2f" + queenbee_id: <%= ActiveRecord::FixtureSet.identify :'37s_fizzy' %> diff --git a/test/models/account/signal_account_test.rb b/test/models/account/signal_account_test.rb new file mode 100644 index 000000000..ee9b5b6d8 --- /dev/null +++ b/test/models/account/signal_account_test.rb @@ -0,0 +1,13 @@ +require "test_helper" + +class Account::SignalAccountTest < ActiveSupport::TestCase + setup do + @account = accounts("37s") + end + + test "belongs to a signal_account via a shared queenbee_id" do + assert_not_nil @account.queenbee_id + assert_equal @account.queenbee_id, Account.new(signal_account: @account.signal_account).queenbee_id + assert_equal @account.signal_account, Account.new(queenbee_id: @account.queenbee_id).signal_account + end +end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 3f86f1cd4..94d4f83dd 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -1,7 +1,4 @@ require "test_helper" -class OrganizationTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end +class AccountTest < ActiveSupport::TestCase end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2cce7f8d6..b999101f6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -3,6 +3,7 @@ require_relative "../config/environment" require "rails/test_help" require "webmock/minitest" require "vcr" +require "signal_id/testing" WebMock.allow_net_connect! @@ -25,6 +26,7 @@ module ActiveSupport fixtures :all include ActiveJob::TestHelper + include SignalId::Testing include ActionTextTestHelper, CardTestHelper, ChangeTestHelper, SessionTestHelper end end From 0da5f8101e6b6071d30038bcc6b202276973cb8e Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 12 Jun 2025 12:54:22 -0400 Subject: [PATCH 03/18] User belongs to Signal::User --- app/models/user.rb | 2 +- app/models/user/signal_user.rb | 7 +++++ ...50612163028_add_signal_user_id_to_users.rb | 6 ++++ db/schema.rb | 4 ++- db/schema_cache.yml | 28 ++++++++++++++++++- test/fixtures/users.yml | 3 ++ test/models/user/signal_user_test.rb | 12 ++++++++ 7 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 app/models/user/signal_user.rb create mode 100644 db/migrate/20250612163028_add_signal_user_id_to_users.rb create mode 100644 test/models/user/signal_user_test.rb diff --git a/app/models/user.rb b/app/models/user.rb index 23b2f1cb2..5e295c4dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord - include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, Transferable + include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, SignalUser, Transferable include Timelined # Depends on Accessor has_one_attached :avatar diff --git a/app/models/user/signal_user.rb b/app/models/user/signal_user.rb new file mode 100644 index 000000000..16bee49bc --- /dev/null +++ b/app/models/user/signal_user.rb @@ -0,0 +1,7 @@ +module User::SignalUser + extend ActiveSupport::Concern + + included do + belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true + end +end diff --git a/db/migrate/20250612163028_add_signal_user_id_to_users.rb b/db/migrate/20250612163028_add_signal_user_id_to_users.rb new file mode 100644 index 000000000..5caddaea9 --- /dev/null +++ b/db/migrate/20250612163028_add_signal_user_id_to_users.rb @@ -0,0 +1,6 @@ +class AddSignalUserIdToUsers < ActiveRecord::Migration[8.1] + def change + add_column :users, :signal_user_id, :integer + add_index :users, :signal_user_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 28b66be58..0c7504173 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_06_11_203906) do +ActiveRecord::Schema[8.1].define(version: 2025_06_12_163028) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -336,9 +336,11 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_11_203906) do t.string "name", null: false t.string "password_digest" t.string "role", default: "member", null: false + t.integer "signal_user_id" t.datetime "updated_at", null: false t.index ["email_address"], name: "index_users_on_email_address", unique: true t.index ["role"], name: "index_users_on_role" + t.index ["signal_user_id"], name: "index_users_on_signal_user_id", unique: true end create_table "watches", force: :cascade do |t| diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 751801e34..2c6a24735 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -993,6 +993,16 @@ columns: default_function: collation: comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: signal_user_id + cast_type: *1 + sql_type_metadata: *2 + 'null': true + default: + default_function: + collation: + comment: - *9 watches: - *22 @@ -2276,6 +2286,22 @@ indexes: nulls_not_distinct: comment: valid: true + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: users + name: index_users_on_signal_user_id + unique: true + columns: + - signal_user_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true watches: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: watches @@ -2327,4 +2353,4 @@ indexes: comment: valid: true workflows: [] -version: 20250611203906 +version: 20250612163028 diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index d12c1a4b3..dc13feca0 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -5,18 +5,21 @@ david: email_address: david@37signals.com password_digest: <%= digest %> role: member + signal_user: 37s_fizzy_david jz: name: JZ email_address: jz@37signals.com password_digest: <%= digest %> role: member + signal_user: 37s_fizzy_jz kevin: name: Kevin email_address: kevin@37signals.com password_digest: <%= digest %> role: admin + signal_user: 37s_fizzy_kevin system: name: System diff --git a/test/models/user/signal_user_test.rb b/test/models/user/signal_user_test.rb new file mode 100644 index 000000000..81071517b --- /dev/null +++ b/test/models/user/signal_user_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +class User::SignalUserTest < ActiveSupport::TestCase + setup do + @user = users(:david) + end + + test "belongs to a Signal::User" do + assert_not_nil @user.signal_user_id + assert_equal signal_users("37s_fizzy_david"), @user.signal_user + end +end From 319e4223e43b77162cf9e7c3e58841548f6133ac Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 12 Jun 2025 17:13:20 -0400 Subject: [PATCH 04/18] Add request details to Current --- app/controllers/application_controller.rb | 2 +- app/controllers/concerns/current_request.rb | 13 +++++++++++++ app/models/current.rb | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 app/controllers/concerns/current_request.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index af52c2fd6..37f663be8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Authentication, CurrentTimezone, SetPlatform, WriterAffinity + include Authentication, CurrentRequest, CurrentTimezone, SetPlatform, WriterAffinity stale_when_importmap_changes allow_browser versions: :modern diff --git a/app/controllers/concerns/current_request.rb b/app/controllers/concerns/current_request.rb new file mode 100644 index 000000000..b7e4af73d --- /dev/null +++ b/app/controllers/concerns/current_request.rb @@ -0,0 +1,13 @@ +module CurrentRequest + extend ActiveSupport::Concern + + included do + before_action do + Current.http_method = request.method + Current.request_id = request.uuid + Current.user_agent = request.user_agent + Current.ip_address = request.ip + Current.referrer = request.referrer + end + end +end diff --git a/app/models/current.rb b/app/models/current.rb index a4d0f0864..f67ef117d 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,5 +1,6 @@ class Current < ActiveSupport::CurrentAttributes attribute :session + attribute :http_method, :request_id, :user_agent, :ip_address, :referrer delegate :user, to: :session, allow_nil: true end From dacb53b8b1330212dba8ed6da348dede329c594f Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 16 Jun 2025 16:40:11 -0400 Subject: [PATCH 05/18] Launchpad login works for existing 37id identities --- app/controllers/concerns/authentication.rb | 17 +++++++++++++++++ .../sessions/launchpad_controller.rb | 12 ++++++++++++ app/models/current.rb | 1 + app/models/launchpad.rb | 16 ++++++++++++++++ config/routes.rb | 1 + 5 files changed, 47 insertions(+) create mode 100644 app/controllers/sessions/launchpad_controller.rb create mode 100644 app/models/launchpad.rb diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 746d3cda5..d47dc44d2 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -2,6 +2,7 @@ module Authentication extend ActiveSupport::Concern included do + before_action :set_current_account before_action :require_authentication helper_method :authenticated? end @@ -16,6 +17,11 @@ module Authentication skip_before_action :require_authentication, **options before_action :resume_session, **options end + + def require_untenanted_access(**options) + skip_before_action :require_authentication, **options + before_action :redirect_tenanted_request, **options + end end private @@ -40,6 +46,10 @@ module Authentication def request_authentication session[:return_to_after_authenticating] = request.url + + # # When we're ready to flip the switch to Launchpad authentication, uncomment this line: + # redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true + redirect_to new_session_path end @@ -51,6 +61,9 @@ module Authentication redirect_to root_url if authenticated? end + def redirect_tenanted_request + redirect_to root_url if ApplicationRecord.current_tenant + end def start_new_session_for(user) user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| @@ -64,6 +77,10 @@ module Authentication cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax } end + def set_current_account + Current.account = Account.first + end + def terminate_session Current.session.destroy cookies.delete(:session_token) diff --git a/app/controllers/sessions/launchpad_controller.rb b/app/controllers/sessions/launchpad_controller.rb new file mode 100644 index 000000000..9409f2c85 --- /dev/null +++ b/app/controllers/sessions/launchpad_controller.rb @@ -0,0 +1,12 @@ +class Sessions::LaunchpadController < ApplicationController + require_unauthenticated_access + + def show + if user = Current.account.signal_account.authenticate(sig: params[:sig]).try(:peer) + start_new_session_for user + redirect_to after_authentication_url + else + render plain: "Authentication failed. This is probably a bug.", status: :unauthorized + end + end +end diff --git a/app/models/current.rb b/app/models/current.rb index f67ef117d..5a85b2e16 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,6 +1,7 @@ class Current < ActiveSupport::CurrentAttributes attribute :session attribute :http_method, :request_id, :user_agent, :ip_address, :referrer + attribute :account delegate :user, to: :session, allow_nil: true end diff --git a/app/models/launchpad.rb b/app/models/launchpad.rb new file mode 100644 index 000000000..8b437c809 --- /dev/null +++ b/app/models/launchpad.rb @@ -0,0 +1,16 @@ +# lifted from bc3 app/models/launchpad.rb +module Launchpad + extend self + + delegate :url, to: "SignalId.launchpad" + + def login_url(product: false, account: nil, **params) + url product_account_path("/signin", product: product, account: account), params + end + + def product_account_path(path = nil, product: false, account: nil) + product_path = "/fizzy" if product || account + account_path = "/#{account.signal_account.id}" if account + [ product_path, account_path, path ].compact.join + end +end diff --git a/config/routes.rb b/config/routes.rb index db0cf3cd6..7cf104da4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,6 +85,7 @@ Rails.application.routes.draw do scope module: "sessions" do resources :transfers, only: %i[ show update ] end + get "launchpad", to: "sessions/launchpad#show" end resources :users do From 73ba0c074f8cef01cd6b0c4041fd507ed4658d7d Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Fri, 13 Jun 2025 11:38:11 -0400 Subject: [PATCH 06/18] Introduce a Signup model and integrate with QB/37id - Creates a Queenbee::Account - SignalId peer classes work properly - Set up a basic template for the account (closure reasons, workflow, collection) - Load signal_id fixtures during `bin/setup --reset` - Update signal_id to pull in the Fizzy product --- Gemfile | 3 + Gemfile.lock | 22 +++- app/models/account.rb | 9 ++ app/models/account/signal_account.rb | 19 ++++ app/models/signup.rb | 124 +++++++++++++++++++++ config/initializers/signal_id.rb | 7 ++ test/models/account/signal_account_test.rb | 23 ++++ test/models/signup_test.rb | 93 ++++++++++++++++ test/test_helper.rb | 10 ++ 9 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 app/models/signup.rb create mode 100644 test/models/signup_test.rb diff --git a/Gemfile b/Gemfile index cc84e2398..e5e3637e9 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,8 @@ gem "queuety", bc: "queuety", branch: "rails4" # needed by signal_id gem "service_concurrency_prevention", bc: "service_concurrency_prevention" # needed by queuety gem "portfolio", ">= 4.6", bc: "portfolio" # needed by signal_id gem "file_repository", "~> 1.4.5", bc: "file_repository" # needed by portfolio +gem "queenbee", bc: "queenbee-plugin" +gem "activeresource", require: "active_resource" # needed by queenbee # Telemetry, logging, and operations gem "mission_control-jobs" @@ -63,4 +65,5 @@ group :test do gem "selenium-webdriver" gem "webmock" gem "vcr" + gem "mocha" end diff --git a/Gemfile.lock b/Gemfile.lock index 780151b61..a1e1ced06 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,15 @@ GIT activesupport file_repository (>= 1.4.4) +GIT + remote: https://github.com/basecamp/queenbee-plugin + revision: bf887b5c128570d1b19a0140146ae9aa4ae2915f + specs: + queenbee (3.2.0) + activeresource + builder + rexml + GIT remote: https://github.com/basecamp/queuety revision: bf7e2a552fb674533d3e092f06dbf9b5d1ebc753 @@ -72,7 +81,7 @@ GIT GIT remote: https://github.com/basecamp/signal_id - revision: 7090b0d518b6656d8c8792da8301146da40f3eae + revision: 129483ccc1d9452fa7713080fcbf0c012b2f4107 branch: rails4 specs: signal_id (4.3.2) @@ -205,6 +214,14 @@ GEM specs: action_text-trix (2.1.15) railties + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activeresource (6.1.4) + activemodel (>= 6.0) + activemodel-serializers-xml (~> 1.0) + activesupport (>= 6.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) @@ -568,6 +585,7 @@ PLATFORMS DEPENDENCIES actiontext-lexical! active_record-tenanted! + activeresource aws-sdk-s3 bcrypt (~> 3.1.7) bootsnap @@ -582,11 +600,13 @@ DEPENDENCIES jbuilder kamal! mission_control-jobs + mocha mysql2 platform_agent portfolio (>= 4.6)! propshaft puma (>= 5.0) + queenbee! queuety! rails! rails_structured_logging! diff --git a/app/models/account.rb b/app/models/account.rb index c562043b2..25b74efde 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -2,4 +2,13 @@ class Account < ApplicationRecord include Entropic, Joinable, SignalAccount has_many_attached :uploads + + def setup_basic_template + user = User.first + + Closure::Reason.create_defaults + Collection.create!(name: "Cards", creator: user, all_access: true) + workflow = Workflow.create!(name: "Basic") + Collection.first.update!(workflow: workflow) + end end diff --git a/app/models/account/signal_account.rb b/app/models/account/signal_account.rb index 99af103bb..861626909 100644 --- a/app/models/account/signal_account.rb +++ b/app/models/account/signal_account.rb @@ -5,4 +5,23 @@ module Account::SignalAccount # TODO: remove the "optional: true" once we've populated the accounts properly belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true end + + class_methods do + def create_with_admin_user(queenbee_id:) + new(queenbee_id: queenbee_id).tap do |account| + SignalId::Database.on_master do + account.name = account.signal_account.name + account.save! + + User.create!( + name: account.signal_account.owner.name, + email_address: account.signal_account.owner.email_address, + signal_user_id: account.signal_account.owner.id, + role: "admin", + password: SecureRandom.hex(36) # TODO: remove password column? + ) + end + end + end + end end diff --git a/app/models/signup.rb b/app/models/signup.rb new file mode 100644 index 000000000..a56a5fcd8 --- /dev/null +++ b/app/models/signup.rb @@ -0,0 +1,124 @@ +class Signup + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + attribute :full_name, :string + attribute :email_address, :string + attribute :password, :string + attribute :company_name, :string + + attr_reader :signal_identity, :queenbee_account, :signal_account, :account, :user + + validate :validate_new_identity + + def initialize(...) + super + + @signal_identity = nil + @queenbee_account = nil + @account = nil + @user = nil + end + + def process + return false unless valid? + + create_signal_identity + create_queenbee_account + create_tenant + + true + rescue => error + destroy_tenant + destroy_queenbee_account + destroy_signal_identity + + errors.add(:base, "An error occurred during signup: #{error.message}") + + false + end + + private + def create_signal_identity + SignalId::Database.on_master do + @signal_identity = build_new_identity.tap(&:save!) + end + end + + def create_queenbee_account + @queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes) + SignalId::Database.on_master do + @signal_account = SignalId::Account.find_by(queenbee_id: @queenbee_account.id) + end + end + + def create_tenant(&block) + ApplicationRecord.create_tenant(tenant_name) do + @account = Account.create_with_admin_user(queenbee_id: queenbee_account.id) + @account.setup_basic_template + @user = User.first + end + end + + def destroy_tenant + if queenbee_account && ApplicationRecord.tenant_exist?(tenant_name) + ApplicationRecord.destroy_tenant(tenant_name) + @account = nil + @user = nil + end + end + + def destroy_queenbee_account + queenbee_account&.cancel + @queenbee_account = nil + end + + def destroy_signal_identity + SignalId::Database.on_master do + signal_identity&.destroy + end + @signal_identity = nil + end + + def validate_new_identity + build_new_identity.tap do |identity| + unless identity.valid? + identity.errors.each { |error| errors.add(error.attribute, error.message) } + end + end + end + + def build_new_identity + SignalId::Identity.new do |identity| + identity.name = full_name || email_address + identity.email_address = email_address + identity.username = email_address + identity.password = password + end + end + + def queenbee_account_attributes + { + skip_remote: true, # Fizzy creates its own local account + product_name: "fizzy", + name: company_name.presence || signal_identity.name, + owner_identity_id: signal_identity.id, + trial: false, + subscription: subscription_attributes, + remote_request: request_attributes + } + end + + def subscription_attributes + { name: "FreeV1", price: 0 } + end + + def request_attributes + { remote_address: Current.ip_address, user_agent: Current.user_agent, referrer: Current.referrer } + end + + def tenant_name + @tenant_name ||= signal_account.subdomain + end +end diff --git a/config/initializers/signal_id.rb b/config/initializers/signal_id.rb index cf88348c0..dc2c41417 100644 --- a/config/initializers/signal_id.rb +++ b/config/initializers/signal_id.rb @@ -1,8 +1,15 @@ require "signal_id" Rails.application.config.to_prepare do + SignalId.product = "fizzy" + SignalId::Database.load_configuration SignalId::Database.default_configuration SignalId::Database.enable_rw_splitting! + + silence_warnings do + SignalId::Account::Peer = Account + SignalId::User::Peer = User + end end Rails.application.config.after_initialize do diff --git a/test/models/account/signal_account_test.rb b/test/models/account/signal_account_test.rb index ee9b5b6d8..62f5e4b87 100644 --- a/test/models/account/signal_account_test.rb +++ b/test/models/account/signal_account_test.rb @@ -10,4 +10,27 @@ class Account::SignalAccountTest < ActiveSupport::TestCase assert_equal @account.queenbee_id, Account.new(signal_account: @account.signal_account).queenbee_id assert_equal @account.signal_account, Account.new(queenbee_id: @account.queenbee_id).signal_account end + + test ".create_with_admin_user creates a new local account and user peers" do + ApplicationRecord.create_tenant("account-create-with-dependents") do + signal_account = signal_accounts(:honcho_fizzy) + account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id) + + assert_not_nil account + assert account.persisted? + assert_equal 1, Account.count + assert_equal signal_account.queenbee_id, account.queenbee_id + assert_equal signal_account.name, account.name + assert_equal account, signal_account.peer + + assert_equal 1, User.count + User.first.tap do |user| + assert signal_account.owner.name, user.name + assert signal_account.owner.email_address, user.email_address + assert signal_account.owner.id, user.signal_user_id + assert_equal "admin", user.role + assert_equal user, signal_account.owner.peer + end + end + end end diff --git a/test/models/signup_test.rb b/test/models/signup_test.rb new file mode 100644 index 000000000..1c77c1748 --- /dev/null +++ b/test/models/signup_test.rb @@ -0,0 +1,93 @@ +require "test_helper" + +class SignupTest < ActiveSupport::TestCase + setup do + @signup = Signup.new( + email_address: "brian@example.com", + full_name: "Brian Wilson", + company_name: "Beach Boys", + password: SecureRandom.hex(16) + ) + end + + test "#process creates all the necessary objects" do + Account.any_instance.expects(:setup_basic_template).once + + assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ") + assert_empty @signup.errors + + assert @signup.signal_identity + assert @signup.signal_identity.persisted? + + assert @signup.queenbee_account + assert @signup.queenbee_account.id + + assert @signup.signal_account + assert @signup.signal_account.persisted? + + assert @signup.account + assert @signup.account.persisted? + + assert @signup.user + assert @signup.user.persisted? + + assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain + end + + test "#process does nothing if a validation error occurs creating identity" do + @signup.password = "" + + assert_not @signup.process + assert_not_empty @signup.errors[:password] + + assert_nil @signup.signal_identity + assert_nil @signup.queenbee_account + assert_nil @signup.account + assert_nil @signup.user + end + + test "#process does nothing if a validation error occurs creating the queenbee account" do + Queenbee::Remote::Account.stubs(:create!).raises(RuntimeError, "Invalid account data") + + SignalId::Identity.any_instance.expects(:destroy).once + + assert_not @signup.process + assert_not_empty @signup.errors[:base] + + assert_nil @signup.signal_identity + assert_nil @signup.queenbee_account + assert_nil @signup.account + assert_nil @signup.user + end + + test "#process does nothing if a validation error occurs creating the tenant" do + ApplicationRecord.stubs(:create_tenant).raises(RuntimeError, "Tenant already exists") + + Queenbee::Remote::Account.any_instance.expects(:cancel).once + SignalId::Identity.any_instance.expects(:destroy).once + + assert_not @signup.process + assert_not_empty @signup.errors[:base] + + assert_nil @signup.signal_identity + assert_nil @signup.queenbee_account + assert_nil @signup.account + assert_nil @signup.user + end + + test "#process does nothing if a validation error occurs creating the account" do + Account.stubs(:create_with_admin_user).raises(RuntimeError, "Account creation failed") + + ApplicationRecord.expects(:destroy_tenant).once + Queenbee::Remote::Account.any_instance.expects(:cancel).once + SignalId::Identity.any_instance.expects(:destroy).once + + assert_not @signup.process + assert_not_empty @signup.errors[:base] + + assert_nil @signup.signal_identity + assert_nil @signup.queenbee_account + assert_nil @signup.account + assert_nil @signup.user + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index b999101f6..622c41a0e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,6 +4,8 @@ require "rails/test_help" require "webmock/minitest" require "vcr" require "signal_id/testing" +require "queenbee/testing/mocks" +require "mocha/minitest" WebMock.allow_net_connect! @@ -34,3 +36,11 @@ end RubyLLM.configure do |config| config.openai_api_key ||= "DUMMY-TEST-KEY" # Run tests with VCR without having to configure OpenAI API key locally. end + +Queenbee::Remote::Account.class_eval do + # because we use the account ID as the tenant name, we need it to be unique in each test to avoid + # parallelized tests clobbering each other. + def next_id + super + Random.rand(1000000) + end +end From 4d7cd3a8a745eff075cfbb4434df6707d795aba2 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 16 Jun 2025 13:24:54 -0400 Subject: [PATCH 07/18] Signup supports using an existing identity --- app/models/signup.rb | 33 +++++++++++++++++++++------------ test/models/signup_test.rb | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/app/models/signup.rb b/app/models/signup.rb index a56a5fcd8..194debfa8 100644 --- a/app/models/signup.rb +++ b/app/models/signup.rb @@ -3,22 +3,27 @@ class Signup include ActiveModel::Attributes include ActiveModel::Validations - attribute :full_name, :string - attribute :email_address, :string - attribute :password, :string - attribute :company_name, :string + # Input attribute + attr_accessor :company_name - attr_reader :signal_identity, :queenbee_account, :signal_account, :account, :user + # Input attributes when not using an existing identity + attr_accessor :full_name, :email_address, :password + + # Input attribute when using an existing identity, output when creating new identity + attr_accessor :signal_identity + + # Output attributes + attr_reader :queenbee_account, :signal_account, :account, :user validate :validate_new_identity def initialize(...) - super - @signal_identity = nil @queenbee_account = nil @account = nil @user = nil + + super end def process @@ -41,8 +46,10 @@ class Signup private def create_signal_identity - SignalId::Database.on_master do - @signal_identity = build_new_identity.tap(&:save!) + unless signal_identity.present? + SignalId::Database.on_master do + @signal_identity = build_new_identity.tap(&:save!) + end end end @@ -82,9 +89,11 @@ class Signup end def validate_new_identity - build_new_identity.tap do |identity| - unless identity.valid? - identity.errors.each { |error| errors.add(error.attribute, error.message) } + unless signal_identity.present? + build_new_identity.tap do |identity| + unless identity.valid? + identity.errors.each { |error| errors.add(error.attribute, error.message) } + end end end end diff --git a/test/models/signup_test.rb b/test/models/signup_test.rb index 1c77c1748..953628d92 100644 --- a/test/models/signup_test.rb +++ b/test/models/signup_test.rb @@ -10,7 +10,7 @@ class SignupTest < ActiveSupport::TestCase ) end - test "#process creates all the necessary objects" do + test "#process creates all the necessary objects for a new identity" do Account.any_instance.expects(:setup_basic_template).once assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ") @@ -24,12 +24,47 @@ class SignupTest < ActiveSupport::TestCase assert @signup.signal_account assert @signup.signal_account.persisted? + assert_equal @signup.signal_identity, @signup.signal_account.owner.identity assert @signup.account assert @signup.account.persisted? assert @signup.user assert @signup.user.persisted? + assert_equal @signup.user.signal_user, @signup.signal_account.owner + + assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain + end + + test "#process creates all the necessary objects for an existing identity" do + signal_identity = SignalId::Identity.find_by_email_address("david@37signals.com") + + @signup = Signup.new( + signal_identity: signal_identity, + company_name: "Team LeMans", + ) + + Account.any_instance.expects(:setup_basic_template).once + + assert @signup.process, @signup.errors.full_messages.to_sentence(words_connector: ". ") + assert_empty @signup.errors + + assert @signup.signal_identity + assert_equal signal_identity, @signup.signal_identity + + assert @signup.queenbee_account + assert @signup.queenbee_account.id + + assert @signup.signal_account + assert @signup.signal_account.persisted? + assert_equal @signup.signal_identity, @signup.signal_account.owner.identity + + assert @signup.account + assert @signup.account.persisted? + + assert @signup.user + assert @signup.user.persisted? + assert_equal @signup.user.signal_user, @signup.signal_account.owner assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain end From a8a64a2384615487be4671e1c02bd56872ff3376 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 16 Jun 2025 15:18:31 -0400 Subject: [PATCH 08/18] Link the seeds up to the signal_id fixture data --- app/models/signup.rb | 15 +++++++------ bin/dev | 2 -- bin/setup | 9 ++++++-- db/seeds.rb | 49 ++++++++++++++++++++++++++++++++--------- db/seeds/37signals.rb | 8 +++---- db/seeds/first-run.rb | 1 - db/seeds/honcho.rb | 16 +++++++------- test/fixtures/users.yml | 2 +- 8 files changed, 66 insertions(+), 36 deletions(-) delete mode 100644 db/seeds/first-run.rb diff --git a/app/models/signup.rb b/app/models/signup.rb index 194debfa8..494dc883a 100644 --- a/app/models/signup.rb +++ b/app/models/signup.rb @@ -26,12 +26,12 @@ class Signup super end - def process + def process(destroy_existing_tenant: false) return false unless valid? create_signal_identity create_queenbee_account - create_tenant + create_tenant(destroy_existing_tenant:) true rescue => error @@ -44,6 +44,10 @@ class Signup false end + def tenant_name + @tenant_name ||= signal_account.subdomain + end + private def create_signal_identity unless signal_identity.present? @@ -60,7 +64,8 @@ class Signup end end - def create_tenant(&block) + def create_tenant(destroy_existing_tenant: false, &block) + ApplicationRecord.destroy_tenant(tenant_name) if destroy_existing_tenant ApplicationRecord.create_tenant(tenant_name) do @account = Account.create_with_admin_user(queenbee_id: queenbee_account.id) @account.setup_basic_template @@ -126,8 +131,4 @@ class Signup def request_attributes { remote_address: Current.ip_address, user_agent: Current.user_agent, referrer: Current.referrer } end - - def tenant_name - @tenant_name ||= signal_account.subdomain - end end diff --git a/bin/dev b/bin/dev index 4099edaa0..e5fc62f6c 100755 --- a/bin/dev +++ b/bin/dev @@ -2,10 +2,8 @@ echo "Access with david@37signals.com / secret123456 on http://37signals.fizzy.localhost:3006" echo "Access with david@37signals.com / secret123456 on http://honcho.fizzy.localhost:3006" -echo "Access first run on http://first-run.fizzy.localhost:3006" if [ -f tmp/solid-queue.txt ]; then - # see also config/environments/development.rb export SOLID_QUEUE_IN_PUMA=1 fi diff --git a/bin/setup b/bin/setup index c7223d499..61727ea2e 100755 --- a/bin/setup +++ b/bin/setup @@ -7,8 +7,13 @@ system("gem install bundler --conservative") system("bundle check") || system!("bundle install") puts "\n== Preparing database ==" -system! "bin/rails db:prepare" -system! "bin/rails db:reset" if ARGV.include?("--reset") +if ARGV.include?("--reset") + system! "bin/rails db:drop db:create db:schema:load db:prepare" + system! "bin/rails db:fixtures:load" # load signal id fixtures before running fizzy seeds + system! "bin/rails db:seed" +else + system! "bin/rails db:prepare" +end puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" diff --git a/db/seeds.rb b/db/seeds.rb index 9af00fe66..a04ee160e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,24 +7,52 @@ def seed_account(name) puts " #{elapsed.round(2)} sec" end -def create_tenant(name) - ApplicationRecord.destroy_tenant name - ApplicationRecord.create_tenant name - ApplicationRecord.current_tenant = name +def create_tenant(signal_account_name) + signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name) + tenant_name = signal_account.subdomain + + ApplicationRecord.destroy_tenant tenant_name + ApplicationRecord.create_tenant(tenant_name) do + account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id) + account.setup_basic_template + end + + ApplicationRecord.current_tenant = tenant_name end -def create_first_run(name, email_address, password: "secret123456") - FirstRun.create!(name:, email_address:, password:) +def find_or_create_user(full_name, email_address) + SignalId::Database.on_master do + unless signal_identity = SignalId::Identity.find_by_email_address(email_address) + signal_identity = SignalId::Identity.create!( + name: full_name, + email_address: email_address, + username: email_address, + password: "secret123456" + ) + end + + signal_account = Account.first.signal_account + signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account) + + if user = User.find_by(signal_user_id: signal_user.id) + user.password = "secret123456" + user.save! + user + else + User.create!( + signal_user_id: signal_user.id, + name: signal_identity.name, + email_address: signal_identity.email_address, + password: "secret123456" + ) + end + end end def login_as(user) Current.session = user.sessions.create end -def create_user(name, email_address, password: "secret123456") - User.create!(name:, email_address:, password:) -end - def create_collection(name, creator: Current.user, all_access: true, access_to: []) Collection.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) } end @@ -36,4 +64,3 @@ end # Seed accounts seed_account "37signals" seed_account "honcho" -seed_account "first-run" diff --git a/db/seeds/37signals.rb b/db/seeds/37signals.rb index 432030bbc..2bc322fdc 100644 --- a/db/seeds/37signals.rb +++ b/db/seeds/37signals.rb @@ -1,10 +1,10 @@ create_tenant "37signals" -david = create_first_run "David Heinemeier Hansson", "david@37signals.com" -login_as david +david = find_or_create_user "David Heinemeier Hansson", "david@37signals.com" +jz = find_or_create_user "Jason Zimdars", "jz@37signals.com" +kevin = find_or_create_user "Kevin Mcconnell", "kevin@37signals.com" -jz = create_user "Jason Zimdars", "jz@37signals.com" -kevin = create_user "Kevin Mcconnell", "kevin@37signals.com" +login_as david create_collection("Fizzy", access_to: [ jz, kevin ]).tap do |fizzy| create_card("Prepare sign-up page", description: "We need to do this before the launch.", collection: fizzy) diff --git a/db/seeds/first-run.rb b/db/seeds/first-run.rb deleted file mode 100644 index e6df219d9..000000000 --- a/db/seeds/first-run.rb +++ /dev/null @@ -1 +0,0 @@ -create_tenant "first-run" diff --git a/db/seeds/honcho.rb b/db/seeds/honcho.rb index 34599c600..939dc9147 100644 --- a/db/seeds/honcho.rb +++ b/db/seeds/honcho.rb @@ -1,15 +1,15 @@ -create_tenant "honcho" +create_tenant "Honcho" + +david = find_or_create_user "David Heinemeier Hansson", "david@37signals.com" +jason = find_or_create_user "Jason Zimdars", "jz@37signals.com" +kevin = find_or_create_user "Kevin McConnell", "kevin@37signals.com" +jorge = find_or_create_user "Jorge Manrubia", "jorge@37signals.com" +mike = find_or_create_user "Mike Dalessio", "mike@37signals.com" -david = create_first_run "David Heinemeier Hansson", "david@37signals.com" login_as david -jason = create_user "Jason Zimdars", "jz@37signals.com" -kevin = create_user "Kevin Mcconnell", "kevin@37signals.com" -sarah = create_user "Sarah Johnson", "sarah@37signals.com" -mike = create_user "Mike Peterson", "mike@37signals.com" - # Array of authors for random assignment -authors = [ david, jason, kevin, sarah, mike ] +authors = [ david, jason, kevin, jorge, mike ] # Card titles for reuse across collections card_titles = [ diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index dc13feca0..3339d12d6 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -12,7 +12,7 @@ jz: email_address: jz@37signals.com password_digest: <%= digest %> role: member - signal_user: 37s_fizzy_jz + signal_user: 37s_fizzy_jzimdars kevin: name: Kevin From 6594dd115ae136d76cd6b77081a49b4ec2dd3bdd Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 19 Jun 2025 17:13:31 -0400 Subject: [PATCH 09/18] ci: run the 37id tests --- config/ci.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/ci.rb b/config/ci.rb index c94990581..bc96a3831 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -10,6 +10,7 @@ CI.run do step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" step "Tests: Rails", "bin/rails test" + step "Tests: 37id", "bin/rails 37id:test:units" if success? step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" From ee60e8dd01f5451b490e9953a32fb149e7f352dd Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 16 Jun 2025 17:10:01 -0400 Subject: [PATCH 10/18] Signup::AccountsController suppports signing up with a new identity --- app/controllers/concerns/authentication.rb | 1 + app/controllers/signup/accounts_controller.rb | 27 +++++++++ app/views/signup/accounts/new.html.erb | 58 +++++++++++++++++++ config/initializers/tenanting.rb | 13 ++++- config/routes.rb | 5 ++ .../signup/accounts_controller_test.rb | 39 +++++++++++++ 6 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 app/controllers/signup/accounts_controller.rb create mode 100644 app/views/signup/accounts/new.html.erb create mode 100644 test/controllers/signup/accounts_controller_test.rb diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index d47dc44d2..dc75fb777 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -19,6 +19,7 @@ module Authentication end def require_untenanted_access(**options) + skip_before_action :set_current_account, **options skip_before_action :require_authentication, **options before_action :redirect_tenanted_request, **options end diff --git a/app/controllers/signup/accounts_controller.rb b/app/controllers/signup/accounts_controller.rb new file mode 100644 index 000000000..e722684ea --- /dev/null +++ b/app/controllers/signup/accounts_controller.rb @@ -0,0 +1,27 @@ +class Signup::AccountsController < ApplicationController + require_untenanted_access + + def new + @signup = Signup.new + end + + def create + @signup = Signup.new(signup_params) + + if @signup.process + redirect_to_account(@signup.account) + else + render :new, status: :unprocessable_entity + end + end + + private + def signup_params + params.require(:signup).permit(:full_name, :email_address, :password, :company_name) + end + + def redirect_to_account(account) + redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path), + allow_other_host: true + end +end diff --git a/app/views/signup/accounts/new.html.erb b/app/views/signup/accounts/new.html.erb new file mode 100644 index 000000000..9f69eba4b --- /dev/null +++ b/app/views/signup/accounts/new.html.erb @@ -0,0 +1,58 @@ +<% @page_title = "Sign up for Fizzy" %> + +
" style="--panel-size: 65ch;"> +

Fizzy

+

Create your account

+ + <%= form_with model: @signup, url: signup_accounts_path, scope: "signup", class: "flex flex-column gap txt-large", data: { turbo: false } do |form| %> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + <% if @signup.errors.any? %> +
+
    + <% @signup.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + + <% end %> + +
+

+ Already have an account? + <%= link_to "Sign in", Launchpad.login_url(product: true), class: "decorated" %> +

+
+
diff --git a/config/initializers/tenanting.rb b/config/initializers/tenanting.rb index 78e5d8e05..ce4193330 100644 --- a/config/initializers/tenanting.rb +++ b/config/initializers/tenanting.rb @@ -2,10 +2,17 @@ Rails.application.config.after_initialize do # in production and staging, we're using a two-level subdomain like "tenant.fizzy.37signals.com". # in development and beta it's only a single-level subdomain. Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do - next nil if request.path == "/up" - tld_length = [ "37signals.com", "37signals-staging.com" ].include?(request.domain) ? 2 : 1 - request.subdomain(tld_length).presence || "return-404" # this is a quick fix for now + + if request.path == "/up" + nil + elsif subdomain = request.subdomain(tld_length).presence + subdomain + elsif optional = request.path =~ %r{^/signup\b} + nil + else + "return-404" # this is a quick fix for now + end end end diff --git a/config/routes.rb b/config/routes.rb index 7cf104da4..ad780b0da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -88,6 +88,11 @@ Rails.application.routes.draw do get "launchpad", to: "sessions/launchpad#show" end + namespace :signup do + get "/" => "accounts#new" + resources :accounts, only: %i[ new create ] + end + resources :users do scope module: :users do resource :avatar diff --git a/test/controllers/signup/accounts_controller_test.rb b/test/controllers/signup/accounts_controller_test.rb new file mode 100644 index 000000000..f7c2a6ad7 --- /dev/null +++ b/test/controllers/signup/accounts_controller_test.rb @@ -0,0 +1,39 @@ +require "test_helper" + +class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest + test "new at a tenanted domain" do + get new_signup_account_url + + assert_redirected_to root_url + end + + test "new at an untenanted domain" do + integration_session.host = "example.com" # no subdomain + + get new_signup_account_url + + assert_response :success + end + + test "create with invalid params" do + integration_session.host = "example.com" # no subdomain + + post signup_accounts_url, params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } } + + assert_response :unprocessable_entity + assert_select "div.alert--error", text: /you need to choose a password/i + end + + test "create for a new " do + integration_session.host = "example.com" # no subdomain + + assert_difference -> { SignalId::Identity.count }, +1 do + assert_difference -> { SignalId::Account.count }, +1 do + post signup_accounts_url, params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: SecureRandom.hex(12), company_name: "signup-accounts-controller-test-1" } } + end + end + + signal_account = SignalId::Account.last + assert_redirected_to(/#{signal_account.login_url}/) + end +end From 237830d91c9575d396463e50c8d9963f8f57e1c4 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 18 Jun 2025 14:39:54 -0400 Subject: [PATCH 11/18] During signup, name beta accounts distinctly because Launchpad will present both in the UI, but will not be able to redirect to a beta fizzy account from production launchpad (or vice versa). --- app/models/signup.rb | 8 +++++++- test/models/signup_test.rb | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/models/signup.rb b/app/models/signup.rb index 494dc883a..40996a1f4 100644 --- a/app/models/signup.rb +++ b/app/models/signup.rb @@ -116,7 +116,7 @@ class Signup { skip_remote: true, # Fizzy creates its own local account product_name: "fizzy", - name: company_name.presence || signal_identity.name, + name: queenbee_account_name, owner_identity_id: signal_identity.id, trial: false, subscription: subscription_attributes, @@ -131,4 +131,10 @@ class Signup def request_attributes { remote_address: Current.ip_address, user_agent: Current.user_agent, referrer: Current.referrer } end + + def queenbee_account_name + name = company_name.presence || signal_identity.name + name += " (Beta)" if Rails.env.beta? + name + end end diff --git a/test/models/signup_test.rb b/test/models/signup_test.rb index 953628d92..0dfd2bd87 100644 --- a/test/models/signup_test.rb +++ b/test/models/signup_test.rb @@ -25,6 +25,7 @@ class SignupTest < ActiveSupport::TestCase assert @signup.signal_account assert @signup.signal_account.persisted? assert_equal @signup.signal_identity, @signup.signal_account.owner.identity + assert_equal @signup.company_name, @signup.signal_account.name assert @signup.account assert @signup.account.persisted? @@ -36,6 +37,16 @@ class SignupTest < ActiveSupport::TestCase assert_includes ApplicationRecord.tenants, @signup.signal_account.subdomain end + test "the new account is named with company name if present" do + assert_equal @signup.company_name, @signup.send(:queenbee_account_name) + end + + test "in beta, the new account is named distinctly" do + Rails.stubs(:env).returns(stub(beta?: true)) + + assert_equal "#{@signup.company_name} (Beta)", @signup.send(:queenbee_account_name) + end + test "#process creates all the necessary objects for an existing identity" do signal_identity = SignalId::Identity.find_by_email_address("david@37signals.com") From 1be9ba6ee3937d48461a4968f5cfda59951683da Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 18 Jun 2025 15:24:50 -0400 Subject: [PATCH 12/18] Signup workflow for existing identity --- app/controllers/signup/accounts_controller.rb | 16 ++++---- app/controllers/signup/base_controller.rb | 30 ++++++++++++++ .../signup/completions_controller.rb | 23 +++++++++++ app/controllers/signup/sessions_controller.rb | 9 +++++ app/models/launchpad.rb | 4 ++ app/models/signup.rb | 18 +++++++++ app/views/signup/completions/new.html.erb | 1 + config/routes.rb | 2 + .../signup/accounts_controller_test.rb | 39 +++++++++++++++++++ test/models/signup_test.rb | 10 +++++ 10 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 app/controllers/signup/base_controller.rb create mode 100644 app/controllers/signup/completions_controller.rb create mode 100644 app/controllers/signup/sessions_controller.rb create mode 100644 app/views/signup/completions/new.html.erb diff --git a/app/controllers/signup/accounts_controller.rb b/app/controllers/signup/accounts_controller.rb index e722684ea..614315ff3 100644 --- a/app/controllers/signup/accounts_controller.rb +++ b/app/controllers/signup/accounts_controller.rb @@ -1,5 +1,5 @@ -class Signup::AccountsController < ApplicationController - require_untenanted_access +class Signup::AccountsController < Signup::BaseController + before_action :reset_signup_storage def new @signup = Signup.new @@ -8,7 +8,10 @@ class Signup::AccountsController < ApplicationController def create @signup = Signup.new(signup_params) - if @signup.process + if @signup.recognized? + store_signup + redirect_to Launchpad.authentication_url(purpose: "signup", login_hint: @signup.email_address, redirect_uri: signup_session_url), allow_other_host: true + elsif @signup.process redirect_to_account(@signup.account) else render :new, status: :unprocessable_entity @@ -17,11 +20,10 @@ class Signup::AccountsController < ApplicationController private def signup_params - params.require(:signup).permit(:full_name, :email_address, :password, :company_name) + params.require(:signup).permit(*Signup::PERMITTED_KEYS) end - def redirect_to_account(account) - redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path), - allow_other_host: true + def store_signup + @signup.to_h.each { |key, value| signup_storage[key.to_s] = value } end end diff --git a/app/controllers/signup/base_controller.rb b/app/controllers/signup/base_controller.rb new file mode 100644 index 000000000..e3d112597 --- /dev/null +++ b/app/controllers/signup/base_controller.rb @@ -0,0 +1,30 @@ +class Signup::BaseController < ApplicationController + require_untenanted_access + + attr_reader :authenticated_identity + + private + def signup_storage + session[:signup] ||= {} + end + + def reset_signup_storage + session.delete(:signup) + end + + def authenticated_identity=(identity) + @authenticated_identity = identity + signup_storage["identity_id"] = identity.try(:id) + end + + def set_authenticated_identity + if identity_id = signup_storage["identity_id"] + @authenticated_identity = SignalId::Identity.find_by(id: identity_id) + end + end + + def redirect_to_account(account) + redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path), + allow_other_host: true + end +end diff --git a/app/controllers/signup/completions_controller.rb b/app/controllers/signup/completions_controller.rb new file mode 100644 index 000000000..4f97055a0 --- /dev/null +++ b/app/controllers/signup/completions_controller.rb @@ -0,0 +1,23 @@ +class Signup::CompletionsController < Signup::BaseController + before_action :set_authenticated_identity, only: :create + + def new + end + + def create + @signup = Signup.new(signup_params) + @signup.signal_identity = authenticated_identity + + if @signup.process + reset_signup_storage + redirect_to_account(@signup.account) + else + render plain: "Could not complete signup. This is probably a bug.", status: :unprocessable_entity, layout: false + end + end + + private + def signup_params + signup_storage.slice(*Signup::PERMITTED_KEYS.map(&:to_s)) + end +end diff --git a/app/controllers/signup/sessions_controller.rb b/app/controllers/signup/sessions_controller.rb new file mode 100644 index 000000000..b33c0cf2f --- /dev/null +++ b/app/controllers/signup/sessions_controller.rb @@ -0,0 +1,9 @@ +class Signup::SessionsController < Signup::BaseController + def create + if self.authenticated_identity = SignalId::Identity.authenticate(params.permit(:sig)) + redirect_to new_signup_completion_url + else + render plain: "Authentication failed. This is probably a bug.", status: :unauthorized + end + end +end diff --git a/app/models/launchpad.rb b/app/models/launchpad.rb index 8b437c809..6001dac82 100644 --- a/app/models/launchpad.rb +++ b/app/models/launchpad.rb @@ -8,6 +8,10 @@ module Launchpad url product_account_path("/signin", product: product, account: account), params end + def authentication_url(**params) + url "/authenticate", params.merge(product: :fizzy) + end + def product_account_path(path = nil, product: false, account: nil) product_path = "/fizzy" if product || account account_path = "/#{account.signal_account.id}" if account diff --git a/app/models/signup.rb b/app/models/signup.rb index 40996a1f4..58cdebcc3 100644 --- a/app/models/signup.rb +++ b/app/models/signup.rb @@ -3,6 +3,8 @@ class Signup include ActiveModel::Attributes include ActiveModel::Validations + PERMITTED_KEYS = %i[ full_name email_address password company_name ] + # Input attribute attr_accessor :company_name @@ -18,6 +20,10 @@ class Signup validate :validate_new_identity def initialize(...) + @full_name = nil + @email_address = nil + @password = nil + @company_name = nil @signal_identity = nil @queenbee_account = nil @account = nil @@ -44,10 +50,22 @@ class Signup false end + def recognized? + SignalId::Identity.find_by_email_address(email_address).present? + end + def tenant_name @tenant_name ||= signal_account.subdomain end + def to_h + { + full_name: full_name, + email_address: email_address, + company_name: company_name + }.compact_blank + end + private def create_signal_identity unless signal_identity.present? diff --git a/app/views/signup/completions/new.html.erb b/app/views/signup/completions/new.html.erb new file mode 100644 index 000000000..2fce66a3c --- /dev/null +++ b/app/views/signup/completions/new.html.erb @@ -0,0 +1 @@ +<%= auto_submit_form_with url: signup_completions_path, data: { turbo: false } %> diff --git a/config/routes.rb b/config/routes.rb index ad780b0da..ae77af74a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,6 +91,8 @@ Rails.application.routes.draw do namespace :signup do get "/" => "accounts#new" resources :accounts, only: %i[ new create ] + get "/session" => "sessions#create" # redirect from Launchpad after mid-signup authentication + resources :completions, only: %i[ new create ] end resources :users do diff --git a/test/controllers/signup/accounts_controller_test.rb b/test/controllers/signup/accounts_controller_test.rb index f7c2a6ad7..5e1eff360 100644 --- a/test/controllers/signup/accounts_controller_test.rb +++ b/test/controllers/signup/accounts_controller_test.rb @@ -36,4 +36,43 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest signal_account = SignalId::Account.last assert_redirected_to(/#{signal_account.login_url}/) end + + test "create for an existing identity" do + integration_session.host = "example.com" # no subdomain + + identity = signal_identities(:david) + + post signup_accounts_url, params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } } + assert_authentication_requested_for identity + + assert_no_difference -> { SignalId::Identity.count } do + assert_difference -> { SignalId::Account.count } do + authenticate_via_launchpad_as(identity) + assert_redirected_to_account + assert_nil session[:signup] + end + end + + signal_account = SignalId::Account.last + ApplicationRecord.with_tenant(signal_account.subdomain) do + assert_equal Account.last, signal_account.peer + end + end + + def assert_authentication_requested_for(identity) + assert_response :redirect + assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url) + end + + def authenticate_via_launchpad_as(identity) + get signup_session_url, params: { sig: identity.perishable_signature } + assert_equal identity.id, session[:signup]["identity_id"] + assert_redirected_to new_signup_completion_url + post signup_completions_url + end + + def assert_redirected_to_account(signal_account = SignalId::Account.last) + assert_response :redirect + assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url) + end end diff --git a/test/models/signup_test.rb b/test/models/signup_test.rb index 0dfd2bd87..fc9f89d7a 100644 --- a/test/models/signup_test.rb +++ b/test/models/signup_test.rb @@ -10,6 +10,16 @@ class SignupTest < ActiveSupport::TestCase ) end + test "#to_h allows persistence of the signup data" do + actual = @signup.to_h + expected = { + email_address: @signup.email_address, + full_name: @signup.full_name, + company_name: @signup.company_name + } + assert_equal expected, actual + end + test "#process creates all the necessary objects for a new identity" do Account.any_instance.expects(:setup_basic_template).once From b29e884b1bcb06215defe308ddec35972fe8f3eb Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Fri, 20 Jun 2025 14:36:51 -0400 Subject: [PATCH 13/18] Rate limit new signups. --- app/controllers/signup/accounts_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/signup/accounts_controller.rb b/app/controllers/signup/accounts_controller.rb index 614315ff3..4c0cf9de2 100644 --- a/app/controllers/signup/accounts_controller.rb +++ b/app/controllers/signup/accounts_controller.rb @@ -1,5 +1,6 @@ class Signup::AccountsController < Signup::BaseController before_action :reset_signup_storage + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signup_account_path, alert: "Try again later." } def new @signup = Signup.new From 6f09d6a737df1c4817ae2cee1c4f3ed7da6b78d5 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 18 Jun 2025 16:08:17 -0400 Subject: [PATCH 14/18] Add HTTP basic auth to the signup controllers so that signup is internal-only for now. --- app/controllers/signup/base_controller.rb | 5 ++ config/credentials/beta.yml.enc | 2 +- config/credentials/development.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/staging.yml.enc | 2 +- config/credentials/test.yml.enc | 2 +- .../signup/accounts_controller_test.rb | 68 +++++++++++++------ 7 files changed, 59 insertions(+), 24 deletions(-) diff --git a/app/controllers/signup/base_controller.rb b/app/controllers/signup/base_controller.rb index e3d112597..ad11d6e1d 100644 --- a/app/controllers/signup/base_controller.rb +++ b/app/controllers/signup/base_controller.rb @@ -1,6 +1,11 @@ class Signup::BaseController < ApplicationController require_untenanted_access + http_basic_authenticate_with( + name: Rails.application.credentials.dig(:account_signup_http_basic_auth, :name), + password: Rails.application.credentials.dig(:account_signup_http_basic_auth, :password) + ) + attr_reader :authenticated_identity private diff --git a/config/credentials/beta.yml.enc b/config/credentials/beta.yml.enc index bff4f4de1..3aaec9278 100644 --- a/config/credentials/beta.yml.enc +++ b/config/credentials/beta.yml.enc @@ -1 +1 @@ -/M5cK3B/Wq1R2f4XsQQ8t7qJGCNMaBU3itDDKNZapeGMjrlE4A9CKOh2RPQsKlrsGjJdgGL4YdfDiT1LiF0pD/KL/t5Fw8Z8GMndzA+hmpvQ+hgufvd8n2ZzK/SIK6RmfC9BabdlvuQ0rr3/EdQJ5yIgk3WtWv73bOWDopqHerEAcaNl8KjCu5n2vokAidqEh0/O30yQQB9PAFMRqEwapLVcDSnaJXr6OQnAAm9tIoWaEaIF8ZJH3Rmn0COs8xZqYFyFv/pP3gVy3Mw+VL3aIkEf/eZ3IQWYNf7fiKPvIdNVsmghmeoQRqCp4E0dpn2XDSEGx5i5oq7ynSDCPky4GahiLtpe3vas+Z+lzD5hAAZmlX0G615b4hWjAViTEQU+CMXhmpj7eVpT9co+1oKmqtbDvHr7uCJIf4d17kC13u/8yjgl2SaCjRWx94wh8su/h6h9Xw0qewITbfM32pFRiH+R5bc=--fxg4Lf+qmYeuvyZp--qVZ+ELV0FRzBb69MUAqOsQ== \ No newline at end of file ++JzvZ6SJZbmUiqTJ6URzPupXIJQ3Sh+RYpjvGgkDizJ+jPQ1j+2d2Dx8AkpGHsKc+lC6Hrt1WiO/jmjNjvTEFvL/TPFh5D+49s0a2/uop3B32519TkTCDHVFWBhiPfOgRyhznvl+b26HR4VmDi0F6d9+C60DVUqMlPf9NptdkdqjafkkC44Dsd3/GfKLJvN8cM69kqom1ug9tLz9W1bvfjlnrS64lfvJc9qKMO2K7Spog1mZM8330AAHVzagzD4lopXHcJysbGfjI9jAQblYq0VFqjy6+bxxT+K/ql4EvYnDpdOcH2ktFopTVUHME/f9d5BCSzElaybNOUk+49Zzl5CUJyEGrVFquV+V9wyDjUrKZ+E06LH3zeCKqQaseSAJdz/Pvp132Deo3pu/WzN0ymq5/lie+JJqgRPzWxsvTrTLe6z/MpxSs3TJq6LY2h1vqZOOhS0kdgAWsHCEcBYGI+6am+D9N+WV4i0FbbTYUGIoxyN2M+PWgeRNes/xCbHo9qhrNle48c4m/Co4rEqR3VaMZnyiztTy6HC43hFD1zpoEuJGBeVHiJEx9I4fn/m+RSiG1qbQGXgZ1DFsm3TLqxB9Ct7SD2gJWmCl--djNicAvuImKuZL9G--6hGmlJLaeEErdqHUQNt8xw== \ No newline at end of file diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index affc4c6cd..eac5ff8bf 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -e6HcyVNJvgdJGTCcvW8FDHQ9pHYmTgNW2/71Espdwxd5LQX/Xb9+VkeqpEAL8ceFJo6ro49mMs85G2YngKbbXEh//8+XSS0inMR/kh9wTV3f6YBWnLVBbhrOnYgPcQ8vedsgxLG2Cs4y7ipfeT/dp9bb5m4IxV0OPiUjVFFJuAuGkMJt3pE7D6alqL1ACRxazV9UKV5WKEBfaQYeKpJ++5f86Nls1EwKo5tB1lJed9P5n2yH7meX/QyxjgQd38CIaPwp+F92y5DLKJD5MZRMJGgWf6MT6096LhjEC3vvtS3wwT/eDVcVKTplRkEszj7BZEfOs9Ondb9EVtbXmeoHYSgpaOq9/nEwtho5Uw==--KlHZy1apg3u1XxFs--N2ehRobLHNz+Sepc8p4EIA== \ No newline at end of file +rzCdhg7Eu/UqyDwyiXWIRCDdQ/jbUZneJT9rpWFQA1hji3RjY1lnmd7CYTppOprDKvStwgRfC//GwGxIXxC8sigImX3+Lzju3d8H1dtq1k98dPBsg1lNRUmmmQ+p8GHJS5M+PXbPnNeDu50TsHjHTQ6n+g2dd/UK0kuJl5RVkoIrdvx1MZ1UCdmbvDjiF3YQf2Mq1FfjGcBqfdwGZLywoE6CbtO70kJrp+G/s/6Sz4en/AYOnrlB/jc/qgOaINzMcv7V0YjlAyzrIGfhJzhe6JsPvdvgRCrStVei4o3NUdJ/13HqDwoik03EUTNaTxjfgnRdLL0MXTw/H0OoOin1ViR12Bx7/3LUWzQtXncRB+2b8FdpO63I3FdG+dRN1c5LPFNCf652ip58a7o2lQcNNQ+ygKJBCVKSBD2oHRqU7aCz6mf3g7RXwhW5Op3p/1iof75w8Lgbkzb1Yf0nWUQGemBjjOYUWwB4NS3c+eEHN+vPVNM=--ZP6IQVw4GNc9MErY--LELFuHAeYiPb3L5I1S2J7A== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index ef3036025..cda315dde 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -Qkkt3krcGlOh76//7TmY5rXz31mhL3NNNiO6TCcRhx4lT6uZ8sLjW7X0yp2wZ30y2cJBeIsNMeOhL47JTpu0Z4d0FPtwgH4bzzXAiab0/YrfMjxyBt7/K1DzW/bePUvakJggIvVGGRA1NB1tPkwziQ4dgA34E/UUPAxUofFaoCIFVABmqt4DFg0DKYB8tImDDjZ5xUfSNxrKj3pH5mOIU9jqSK6LtBELFZcNxrJOI0UEgayRNwYAs44ujdM2/2zrAWlUCFZ3NnMU4bBJW02/d6LaxHtB809u8mx3bChEzWMtZB9CvpbRH7u6XfZJR1YGFztcNguzRbNX5x9pKZImAuAhVFKR7Fbi12Hg3w7U3A/YovwmyhHK6ksfOzYlYoDUTBnGYVwiqR16naaHCMekomK+uo0qtOn83RQkLuuqFJshPXn/lW8Rggt4dq/0THEl7Qj+dumadlWDVEgxhipI031RSBF1toNWw/zwaC31Ord1YXD8ZC09sPoj8Zd4hD2o2a0IUuIMXA8w5/2+AmdU8wF66opdKGdsZDRjM2Bul8aAbJ7/iV7xTuvlE4LmzXGArMvivIZ0GwrN3YAsDta2Y3ufibq7WZzSCOrpbPKJ9CEGg4a2AN8UGaGZ5ZEAYg8lhDgsZMjHRkGNuxk8QkDiVFDyNoI7H9ZQqbw+L1AJRTfDIcNB/9h5n+//Nk4ylLF1lnI7y/IQA/AuSQ+UN+okt3RhLugwGvWrs49MExJStLHpGveSoQ==--MQ2JycR3X6LIiKSY--45C7bleKX9FMGv4fc+G1yw== \ No newline at end of file +wZrcTr2xjFlI9oupBoPLtCfZNJAZPRTDoCWR2+Ak7+QnoI/O527bDPYWWjYJgABWSw/O6NyblXQUwcfWuccKdvGDeTiZkziZkzsPOr192uYk4Wvf/uWpVexvZeFNM7AT5azPVNprCNPKdbBCeyjSl1ETchjA432b7tm8wjO3sxD+3iRPy4lcdSfkz6tX8JIvKO5JMakx/ooNFTovir4TgiImSxNxkRUixHehrvm36pcBFqdbCnK5Lf/jd2rNGelBkfJC/W7GYcmNlGokMQ85rTw/xjkhJXgmZgXNO7TwvKQNcHFUM6B6F1opb06TtaVzWA0VlUot2All3lBef4FKnDN6vXOVoxDnp5E2JkmgmbVd+jxGI3MOxvFh3ENzRHwjuJSL+vaX5bYBBEwPfEDRrF+If9mys1pj6VsJ4KYLvL7dLHLijFUQbLuXrEJpUAp0wk/mPP1ck7Lo1Wa3NbK9YcwoRMTezpIzKmoJuanm2xnXs+uGDDtZ7i/YSk6korQeW7rnuKBie/0QH9hfxYe2PfxzeGGoFd6l1HfHVtLjnWp1Sd/2ItGR2ATJzQVDCfDo2MM2EyiPawwtKL3nLWc0eToWfydmB/Xw82oh9JLqMFFCjuC1pasHLi2OQVicPSSKuBdcnOh8Pji0MoXHdYZ8R4rFTebUQExeVrFRFaPViKaU6hmzwzY9bXgAxWnlYx6K/envcCA5vmhxooEZF6lin+hRfNA+EblP2R6vcvOmojV0mPRumjQESQlUvZz5+KlzoRcPqBmfY9j8KKDZMs7k9SP5nAn00OBeuy6gia0l8zHuII5MYGC3xnOVPJT56OPZlUYB9Es8BVy5yHr9ILCLrg8QzkS058pHLV7HmmG4usNqjIvdPxbfPfTad+s=--GUrt5KBBpPYkq2Jk--vgtaD0duoW8vyd3wakpW8g== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 8387cc6d8..7833f4d5a 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -Tc514fYVlm1c2c81KUFmuAGZ9rSZT7XfeGUEdTEvJNDD6JE9IghKbAAGgc6AalIt2/VxzSV6AGvNagb0/qlJrMOFeSJLqzufKt1YoLRUZtSUZDtzmI/2WSzju0Tyu4za6rnLEu+WvDc0NM8KZEv/TwwUbSOwoEVDQmava/iuWyvFr5sOH1wNr2kwYbErwDpThKcYqacI4CdvgpgXc//3T0Z5vsVCs6d8723DymM0KkSK9so8WrcCesk2fmhM1K2hQV7QdckaVuOL0JEj1AI7KzdJm7REoeLwRwaBeVDkqDNo2fKxwZ2XmU2ViJOozPsowx+324wQQZjbsf/0ohtDkgjbfMmA2tjkuNJdJFwIIc5VyvILhVs5WQIJHSXmabhUKxzKRCRZNBSaadLGCb7/AGfULKvh2vP1BWqm80EkDmhHm42ucJW1vA377y9UYWVcdvqWtp5m2F7HlXqi9Uw/+kFV2X6Jw1c07FsCu35TrUb5u8G7mu/866GBgf5VlfDVs1BA3aru05CtcsXi2qKMw0II7fZps/4XpYCB+VehXAyLi2ABVeTnqSfF7HwasXvfD3Jgh7zJLD0hudNDpu6xkmoyeksVBn034dmQf0TwAIKmNjEKMDxgAG50T2JhnEvHZEnqzGl6O8if8DKM5rPgt3XEU3zN/U3yA8T0eTyFdGlWbYK7Ergu4TcGo3WPk6bJhTGOHQ9puD89t6JGHbKDYW77w44SHv+C42flmNkzcndXlpvZvnEafDYeW6ofRC4eeTRX4+WKL4CYhlel78+twD6Mh0MV42MYM2rmtl66NHN8fWxyfoyjrNcXveKVAsGMatB0+dOglq6GmOiXRAt/I6D+7QYZLyaUhYNmmiuZXF4=--H6KWPnHc9qUQE5AI--PN98cg9t9ZhxMUEax/Mfgg== \ No newline at end of file +bVG7BIEvGCbJZb/VfrNfO0wc+Brny62Xj0oVSPcIe//kVUCQ+4QlDptS8UiRtpO85yESOPlSk/SRKIDEhiGpuXnTt4YQt+Y2V3GTLk9L3sNK9ctetIJUISwvfWuI8syieowJi4g3hOHrBy8aMdpdZWVVoZFJvuTwHZJqPEaS7V2vPfoPaKI00b6il0sp3kML/D6U2QpG4tEruUsB4Ou55RLTXrllzPtrTO5oDdaWQi+7P0OAcOjeORgMIhQM3Dl8n6VM89upjK/W1jt8JapJQsJH4UQYFaY5SRTXljFA478M+Cm5dcG4L7nEzJgMIMLlT7fuPFfu2FsTUwbWvOYxse7r4VzOaEJSxsl8inOeI0aWZYOcaGEpyAD24XOYB23zWtvEtoG3T1kJdQu/pl3ItNcCN4aDeHsrTkY71VTF+WLfZtUt/oIhxCEzOPA525CY9lNQ+ev9PkjiWOUyZF3260Z5JmYWcQ03baESJ74yq2N+/l3Kjgs5NGJK5TVWCiVLiik6Muq9BVrmgH0192ikrZRWMQBFoq9bUk2nn/8GU9V57OqbhXhmC+PcKEFMwGi08JaAlcP6MFvvYpLTPuYeXglDK9zj1NaRmC/6xwlTMgbvVthm+a/HQyLWYndSTTOkifDU7G6mqI1A4vwLiG1rHHH7sRHGbTr3bFwJz0Z6vGqS4ZWL8Ju8WlxeaaZq7YOpfVsifaM82Exx8FlOspt0X2yhoQdR/bUkcpxSOQh6cPDZKulWOFJQ4u2q0GbSU4K1sXNzewfRYQdKwS8i3ezzhKinVrTiuZmlIRfvuBojkWM5xXi5OtoaZGoWlbGlypqvtxEjr5HjQlj9MirvtXlyxMZ2xFwyU8CBAKusEDP6OKt9KK9BulF8U3b95hi0oiPv4qnRxpLv+rV2FKU9dK6K9NA+xiZ61LQ7FUvasbK5sB0BjYbaInczb05yZsQB1tM8vAjLxgLALPbPJE57TGRsks/bBdJVJaNUCoiv9ANRqtZuG/K2vDib--ZaLFpBm5vEIDM+lC--mN7WS01CW/MS2shsMixZ9g== \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index 24d6d9c49..8c7aab16b 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -1QS4QGl05L2YvkEusirttYv2gIfRbNLEWTK7pOXG/HxqyWbWj9o5/GbTFeLI7K0sbcEjz+gcPcVKB+FGnkJECExynSvIN02bg5gP0QegTC3WA2vWInlr+gpoWCNgsazOgHKMxDBnQXc2myqhwDLKRsDHCQTW/5lIiKGBtBOLaml8KrJg84SeYCh54tJUjAp5yRkrhJQBHVgSSUfXRaFcrmdbPls5FDgB+rErACpp4BgYdXZoDds2RITDgQ==--kpoxQbOswL3cInhK--r+dB3hJHvjfUH1EBNzBLSg== \ No newline at end of file +0EWG0dpq/qELmTCqAgzEL2BZH7JCNFe6DDIynyKE06qiBCYhHs5+fMjmpzN0uwz43tqAAKJdhO/hZY4jXmTpxu+LSjwrUKAlxsWvSGu4vHEbJ5Fe+BzhnS84OFse6ma9akUAHaed3gt31wtrt2Y4AbSPedNvriqfYln7cvpgWbvjTgtEbHrcNMfjCBnaXXdO3n1zILunILgLiV7oBW0qurIrl2xOc4mbj4AqVAIElyh0md9fFtuBfsaRj2U9x/+7McbgTFODhHfeqoErEw7fc1RBFrNi2YNuHBSSqegQ6WtWbwq1Yg2Juv/Lw3g4IIdWadJsW3TdTMMiwmBMzOikx3lNX1Z399glvGNFVnJfBubKf6BxvFfAQ85PBzOZPl+/PTs=--GY44JMs5ZO1/eL6G--EhxEWuitiex6eHROpPQamw== \ No newline at end of file diff --git a/test/controllers/signup/accounts_controller_test.rb b/test/controllers/signup/accounts_controller_test.rb index 5e1eff360..270edab58 100644 --- a/test/controllers/signup/accounts_controller_test.rb +++ b/test/controllers/signup/accounts_controller_test.rb @@ -1,16 +1,16 @@ require "test_helper" class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest - test "new at a tenanted domain" do + test "new under a tenanted domain redirects to the root" do get new_signup_account_url assert_redirected_to root_url end - test "new at an untenanted domain" do + test "new under an untenanted domain is OK" do integration_session.host = "example.com" # no subdomain - get new_signup_account_url + get new_signup_account_url, headers: auth_headers assert_response :success end @@ -18,7 +18,9 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest test "create with invalid params" do integration_session.host = "example.com" # no subdomain - post signup_accounts_url, params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } } + post signup_accounts_url, + headers: auth_headers, + params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } } assert_response :unprocessable_entity assert_select "div.alert--error", text: /you need to choose a password/i @@ -29,7 +31,15 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest assert_difference -> { SignalId::Identity.count }, +1 do assert_difference -> { SignalId::Account.count }, +1 do - post signup_accounts_url, params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: SecureRandom.hex(12), company_name: "signup-accounts-controller-test-1" } } + post signup_accounts_url, headers: auth_headers, + params: { + signup: { + full_name: "Jim", + email_address: "jim@example.com", + password: SecureRandom.hex(12), + company_name: "signup-accounts-controller-test-1" + } + } end end @@ -42,7 +52,9 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest identity = signal_identities(:david) - post signup_accounts_url, params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } } + post signup_accounts_url, headers: auth_headers, + params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } } + assert_authentication_requested_for identity assert_no_difference -> { SignalId::Identity.count } do @@ -59,20 +71,38 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest end end - def assert_authentication_requested_for(identity) - assert_response :redirect - assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url) + test "actions require HTTP basic authentication while we're in internal-only mode" do + integration_session.host = "example.com" # no subdomain + + get new_signup_account_url + + assert_response :unauthorized end - def authenticate_via_launchpad_as(identity) - get signup_session_url, params: { sig: identity.perishable_signature } - assert_equal identity.id, session[:signup]["identity_id"] - assert_redirected_to new_signup_completion_url - post signup_completions_url - end + private + def auth_headers + { + "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials( + Rails.application.credentials.dig(:account_signup_http_basic_auth, :name), + Rails.application.credentials.dig(:account_signup_http_basic_auth, :password) + ) + } + end - def assert_redirected_to_account(signal_account = SignalId::Account.last) - assert_response :redirect - assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url) - end + def assert_authentication_requested_for(identity) + assert_response :redirect + assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url) + end + + def authenticate_via_launchpad_as(identity) + get signup_session_url, headers: auth_headers, params: { sig: identity.perishable_signature } + assert_equal identity.id, session[:signup]["identity_id"] + assert_redirected_to new_signup_completion_url + post signup_completions_url, headers: auth_headers + end + + def assert_redirected_to_account(signal_account = SignalId::Account.last) + assert_response :redirect + assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url) + end end From d29bfb9f5ad764344a679d159b80973658e55b99 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 19 Jun 2025 12:19:27 -0400 Subject: [PATCH 15/18] Add a QB API endpoint for plans - add the shared api token to all environments - add a single FreeV1 subscription - add QB routes - allow the queenbee routes in the tenanting middleware ref: https://3.basecamp.com/2914079/buckets/37331921/todos/8746302763 --- app/models/signup.rb | 3 ++- app/models/subscription.rb | 9 +++++++++ config/credentials/beta.yml.enc | 2 +- config/credentials/development.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/staging.yml.enc | 2 +- config/initializers/queenbee.rb | 6 ++++++ config/initializers/tenanting.rb | 2 +- config/routes.rb | 2 ++ test/models/account/signal_account_test.rb | 3 +++ 10 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 app/models/subscription.rb create mode 100644 config/initializers/queenbee.rb diff --git a/app/models/signup.rb b/app/models/signup.rb index 58cdebcc3..e11ab3bfd 100644 --- a/app/models/signup.rb +++ b/app/models/signup.rb @@ -143,7 +143,8 @@ class Signup end def subscription_attributes - { name: "FreeV1", price: 0 } + subscription = FreeV1Subscription + { name: subscription.to_param, price: subscription.price } end def request_attributes diff --git a/app/models/subscription.rb b/app/models/subscription.rb new file mode 100644 index 000000000..cf9113b54 --- /dev/null +++ b/app/models/subscription.rb @@ -0,0 +1,9 @@ +class Subscription < Queenbee::Subscription + SHORT_NAMES = %w[ FreeV1 ] +end + +class FreeV1Subscription < Subscription + property :proper_name, "Free Subscription" + property :price, 0 + property :frequency, "yearly" +end diff --git a/config/credentials/beta.yml.enc b/config/credentials/beta.yml.enc index 3aaec9278..9cc8ba327 100644 --- a/config/credentials/beta.yml.enc +++ b/config/credentials/beta.yml.enc @@ -1 +1 @@ -+JzvZ6SJZbmUiqTJ6URzPupXIJQ3Sh+RYpjvGgkDizJ+jPQ1j+2d2Dx8AkpGHsKc+lC6Hrt1WiO/jmjNjvTEFvL/TPFh5D+49s0a2/uop3B32519TkTCDHVFWBhiPfOgRyhznvl+b26HR4VmDi0F6d9+C60DVUqMlPf9NptdkdqjafkkC44Dsd3/GfKLJvN8cM69kqom1ug9tLz9W1bvfjlnrS64lfvJc9qKMO2K7Spog1mZM8330AAHVzagzD4lopXHcJysbGfjI9jAQblYq0VFqjy6+bxxT+K/ql4EvYnDpdOcH2ktFopTVUHME/f9d5BCSzElaybNOUk+49Zzl5CUJyEGrVFquV+V9wyDjUrKZ+E06LH3zeCKqQaseSAJdz/Pvp132Deo3pu/WzN0ymq5/lie+JJqgRPzWxsvTrTLe6z/MpxSs3TJq6LY2h1vqZOOhS0kdgAWsHCEcBYGI+6am+D9N+WV4i0FbbTYUGIoxyN2M+PWgeRNes/xCbHo9qhrNle48c4m/Co4rEqR3VaMZnyiztTy6HC43hFD1zpoEuJGBeVHiJEx9I4fn/m+RSiG1qbQGXgZ1DFsm3TLqxB9Ct7SD2gJWmCl--djNicAvuImKuZL9G--6hGmlJLaeEErdqHUQNt8xw== \ No newline at end of file +L6piL2TTSQHF7FMlMJRMK6JEUXI9AY6/DPMzr8wrwABiQ18LbcTNnLcDGjHvfC0PZCIQ7H355xnVQdDqDyyJhkFcrW91pW2yS17TI2ULQxgMAi28QR5K+rx2rBV/d665aWIpv7xgi6PgZuiZDAiu4p3ikheTBIo9/iEUa60qtOxsh66nI3hJGHnUhfOd0jhIxL2+ncDnLcWg3jaHFTj7zwmoTrD/npxWAkdmwdA8K6czjZv2UiV004ECCaOAS8rG2eF2BStIYoknTTOrmKhnHhSJbsYHCWCxgrD0pKIf8/5fjbyj/NKP578Qb8XTmMWi01sFM0APT1Nl2xX+4VzbeHNPHhy7BvtTfzDP5nr/vu0d7/1IpR9gYegzVIye5AWzNOYavG73Bz+lyCehcZ/EXuT1smNt/zPtuZMsxrfQ9i9Tv2BqkDTQ5vnz+BIGL7uNFdC4Mu7n4f1FjYdOhHVYqUv5MMITlkskRrx1uOWAGL6WwYdhXfzakFg02+OjEucNjAQIYjy3rglVBR/48zDVnpHfOKce2+3WfERSWbHHe+VnusQRFn53/KyeLGCHfNPUeZZ6RWh7OIm6T+aJFG7fJAJINt1Yoykm9zxi29Dq/+QMgW39dfIkf2ddTSFHzT4w6TKsVSfzvyfnB6L2Che15/Z8RMb/3iUALW/n5Vs11Iy1UrfTIdZ31a07yRfPr7NU4eGpE0Ee7UqMt/ALVqoj4tm9rTnzJm54im6czX27brLm3iHDi01973q/VpLlH8hDP56rBpIY0tEI0vPE47VX01wq/w4/FvHIIMuLy/qphes=--M7iv5/9byEuFFvYy--rJ+PG5eQLtQ2+KbGBi7gvA== \ No newline at end of file diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index eac5ff8bf..ce2cc3c0f 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -rzCdhg7Eu/UqyDwyiXWIRCDdQ/jbUZneJT9rpWFQA1hji3RjY1lnmd7CYTppOprDKvStwgRfC//GwGxIXxC8sigImX3+Lzju3d8H1dtq1k98dPBsg1lNRUmmmQ+p8GHJS5M+PXbPnNeDu50TsHjHTQ6n+g2dd/UK0kuJl5RVkoIrdvx1MZ1UCdmbvDjiF3YQf2Mq1FfjGcBqfdwGZLywoE6CbtO70kJrp+G/s/6Sz4en/AYOnrlB/jc/qgOaINzMcv7V0YjlAyzrIGfhJzhe6JsPvdvgRCrStVei4o3NUdJ/13HqDwoik03EUTNaTxjfgnRdLL0MXTw/H0OoOin1ViR12Bx7/3LUWzQtXncRB+2b8FdpO63I3FdG+dRN1c5LPFNCf652ip58a7o2lQcNNQ+ygKJBCVKSBD2oHRqU7aCz6mf3g7RXwhW5Op3p/1iof75w8Lgbkzb1Yf0nWUQGemBjjOYUWwB4NS3c+eEHN+vPVNM=--ZP6IQVw4GNc9MErY--LELFuHAeYiPb3L5I1S2J7A== \ No newline at end of file +hHi9VfqfZI57GYR5r75Rh00L3FYeoUgsdtCgBzJGdPhhGZLGlCgUAcY6tgVhqhgM/u5WxyPAzmoV+bbQKb0wO0U6KUmrDKDv9uRnNSUjXGYsg8gWdTUTfx7WBFOFawV3p1Dsm7zM1oUJbRWgy2TiMFl5kP9CN4Lk78n5a1/Y7mnx/wmzwUyQv+BUFJ0UKCfRmWieaNoLkm5kWwyepWHaSRU5EVda6J9VtuwP3hISIzncXm0jpr9r/tJm8flgUR3o2STIVHVgg+YcWz0j3st5RMv7i3D2OHfNGx84vsf3LNlsiLGB1ZU0IsmchLOSqPmlDK2oiB7UKepTPthGUFzNmcDG4LpUQfNwn7B8P7by0zadVefdLSB3kFCNLX+KCAbcjHPlWufkagwHo+dV+iIVACC+hfWl3L/IouqYS0ln2XCBDz6A8lI8PHeGaJe9UhfA5JEuJLRPui8znoq7LrKo4/oBB7ArKdHz/W/oHJA9+aLHcLOq9TPSExAsGi1wcjdgF0nk5mB9iX+az3//1zM64rRz8EyzNkBg1I6PV6wpzROLubttv1+I5nRVe4BernNfVJPi/hbvrlrcZmouU32oOzd2aZuPL5/H8gBhMOPlN25Rr51XCziaczgdEfOwTO8da6r7pxsJDkrvKi+Ju6efxhZ6GeotWJuEUtQtWhHnzvhcJAXfUtJf6A==--LWtZauLkWq+tD+4L--mkMcqYebO9TDi6RTRNUQvA== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index cda315dde..00bed20cf 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -wZrcTr2xjFlI9oupBoPLtCfZNJAZPRTDoCWR2+Ak7+QnoI/O527bDPYWWjYJgABWSw/O6NyblXQUwcfWuccKdvGDeTiZkziZkzsPOr192uYk4Wvf/uWpVexvZeFNM7AT5azPVNprCNPKdbBCeyjSl1ETchjA432b7tm8wjO3sxD+3iRPy4lcdSfkz6tX8JIvKO5JMakx/ooNFTovir4TgiImSxNxkRUixHehrvm36pcBFqdbCnK5Lf/jd2rNGelBkfJC/W7GYcmNlGokMQ85rTw/xjkhJXgmZgXNO7TwvKQNcHFUM6B6F1opb06TtaVzWA0VlUot2All3lBef4FKnDN6vXOVoxDnp5E2JkmgmbVd+jxGI3MOxvFh3ENzRHwjuJSL+vaX5bYBBEwPfEDRrF+If9mys1pj6VsJ4KYLvL7dLHLijFUQbLuXrEJpUAp0wk/mPP1ck7Lo1Wa3NbK9YcwoRMTezpIzKmoJuanm2xnXs+uGDDtZ7i/YSk6korQeW7rnuKBie/0QH9hfxYe2PfxzeGGoFd6l1HfHVtLjnWp1Sd/2ItGR2ATJzQVDCfDo2MM2EyiPawwtKL3nLWc0eToWfydmB/Xw82oh9JLqMFFCjuC1pasHLi2OQVicPSSKuBdcnOh8Pji0MoXHdYZ8R4rFTebUQExeVrFRFaPViKaU6hmzwzY9bXgAxWnlYx6K/envcCA5vmhxooEZF6lin+hRfNA+EblP2R6vcvOmojV0mPRumjQESQlUvZz5+KlzoRcPqBmfY9j8KKDZMs7k9SP5nAn00OBeuy6gia0l8zHuII5MYGC3xnOVPJT56OPZlUYB9Es8BVy5yHr9ILCLrg8QzkS058pHLV7HmmG4usNqjIvdPxbfPfTad+s=--GUrt5KBBpPYkq2Jk--vgtaD0duoW8vyd3wakpW8g== \ No newline at end of file +R0KrQNC0YJq8Wiw/KEnP/UrW4mnbSOzaf9XupVUSr5N0E6sHCG9qTzM1eQUWoM2kLAdXjRzjHctg9o8qTOpMrs4PSEOn7LA4EELKs6TJ+pb2P6hM0pjAFyllUrGarR+d/AKkdVejYALIyPO0Hd7WeT+Zk2cS3u8YGjZwKcYJo7csBbHQL/Lx8Vn5yj2AR53khun1QeLL3Y7Yly9/1RK791of0p4F0TzjSZMq5WlxDP1seMiOpXRo/GLg+ZgkHfqWo4zqNnlovySHlVFM84MGYVqF/uf/4DnhfeD90n0MKNCUN2fKfZntEsoZ9FrdzAMzAcDPkocxBWMWvvC235hRlATEDWGmAsxR8Hv5T/KKdbChK3TGnry4k32IQ6uLxsFIrHnX+jzpPre+Zi7ObxAyiUU2f5UatfSBasocIfJwceh/qiLCifzPSeRg3aXfrZLZ+DrPgSHnzw4wO6sxt/S4KlQfQLfPtI5gmO/+T4tHYuVV4EZZLePiigOfFbhuLgbFS1+4Hxi+iOrMyxefcfMieGz5i/e+hTqvcZyKtI04zO0v6MsADKNayCAnVceShh6HNJ5sNUgy4eOSnYA0u9MkQstYj0fVZZVlaU7FXFZ1UgQJjnQYifwxKLk1qTSiQA618yuoRyVSMBWkXDpuAXeJKi6/BVTZ7NgFpratCL+j89c5nzOOR+F8U8F/o6n7BTc75nFgVsYvMb5NWLlSNmCqhMsDnn27FAMzpZ3yzg7SHFirMrgLXWgMGq548AhiFfoavMFLfvxq4Ft6+/ay5dSdCrRV73DBJYED4RPCv+5nIYraqEXd2AGUnomw+bjTlR5ayEIHX5c+kvbwCZjtsaVczeAYzSmIdthEFgJzO89duazlT+w7tHywQb4c5/JUE9RRos3f3ghJihTGXKdlkwFaIPt745dgkZesCSZVGV1osqF+VAq9Wd8heGzjrOwlf0dyMHVL1XQMhGDglh/AAtzOQnJrpZEn1NZweBo9Z+Dt7juLrkpjWjmuH/psBpeUQOUEiEFi1146c9csV/fcVVZHLT6Btj5vuTvKZOjrW7sEAGZW2c138j6t9tYbAJ8CxjniSw==--IShq57WkyKWLCJ99--ybcm6oObxU+anqaQD5lOeg== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 7833f4d5a..1dd42d7b4 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -bVG7BIEvGCbJZb/VfrNfO0wc+Brny62Xj0oVSPcIe//kVUCQ+4QlDptS8UiRtpO85yESOPlSk/SRKIDEhiGpuXnTt4YQt+Y2V3GTLk9L3sNK9ctetIJUISwvfWuI8syieowJi4g3hOHrBy8aMdpdZWVVoZFJvuTwHZJqPEaS7V2vPfoPaKI00b6il0sp3kML/D6U2QpG4tEruUsB4Ou55RLTXrllzPtrTO5oDdaWQi+7P0OAcOjeORgMIhQM3Dl8n6VM89upjK/W1jt8JapJQsJH4UQYFaY5SRTXljFA478M+Cm5dcG4L7nEzJgMIMLlT7fuPFfu2FsTUwbWvOYxse7r4VzOaEJSxsl8inOeI0aWZYOcaGEpyAD24XOYB23zWtvEtoG3T1kJdQu/pl3ItNcCN4aDeHsrTkY71VTF+WLfZtUt/oIhxCEzOPA525CY9lNQ+ev9PkjiWOUyZF3260Z5JmYWcQ03baESJ74yq2N+/l3Kjgs5NGJK5TVWCiVLiik6Muq9BVrmgH0192ikrZRWMQBFoq9bUk2nn/8GU9V57OqbhXhmC+PcKEFMwGi08JaAlcP6MFvvYpLTPuYeXglDK9zj1NaRmC/6xwlTMgbvVthm+a/HQyLWYndSTTOkifDU7G6mqI1A4vwLiG1rHHH7sRHGbTr3bFwJz0Z6vGqS4ZWL8Ju8WlxeaaZq7YOpfVsifaM82Exx8FlOspt0X2yhoQdR/bUkcpxSOQh6cPDZKulWOFJQ4u2q0GbSU4K1sXNzewfRYQdKwS8i3ezzhKinVrTiuZmlIRfvuBojkWM5xXi5OtoaZGoWlbGlypqvtxEjr5HjQlj9MirvtXlyxMZ2xFwyU8CBAKusEDP6OKt9KK9BulF8U3b95hi0oiPv4qnRxpLv+rV2FKU9dK6K9NA+xiZ61LQ7FUvasbK5sB0BjYbaInczb05yZsQB1tM8vAjLxgLALPbPJE57TGRsks/bBdJVJaNUCoiv9ANRqtZuG/K2vDib--ZaLFpBm5vEIDM+lC--mN7WS01CW/MS2shsMixZ9g== \ No newline at end of file +ver09XH5FQD8dviqbC6DwvttqfTaL0UGRVtoS+qebJx22xjuaMSPXva21pCBj1vayxfEYytSz10/IQ/gYc6np/Th6lCLyljV6wnNigjgSRLYU9se9yU0yf3j6UmlDUphkxt6VGhXd/lS162rJnAlVPqu1+wBboqYuRcgeeGnnxbigKV+DnRpoQFeFy6rohMQNr0HFxfUbAMkkGWstAbKjWpOeeQrnAaj6jU7Ri4NnkF/I6KgeU1s0Wq95Kqe4ghytYHHBenTdIKOjZq7lqvnnt3TmOO5lJokA10r8ukLKrJ06BB56Y9HtrAYjrBGL4ky2bGYnvVS3EjSy7Vep5g5UO/ND5y353HCOwPuh8jxMudKhXfZoSRxl2QB4BDwWSTnALeX0t+KxHEbaz6u4q86miU2UseEgflA7Tx91WzYvA5aSJI81Y+TsFXTvBkU8BEqKiyA+BvcJA8kkAJYsbn9X7Vr36RBOPk9vhgEiDNp8ITStk/qEs2w446Dhnfjthce/UtaqDGHrm5mu3vR0QZa2Fo3rbBvhwx4uHJhHh163wJ9xdlIQ20PvuZFGf1sE2NdHD+d/dG3godJis2qyvz0wFIok5gW39hH3gTokroOaxiF0HE0rPiN7oVkBpSukeEHSXHE/X2+E/3NsjB5vDUtzcQASTrMKs1pldSsy9Fz0qwsKmq3W04qhs8DW94PzqhYONJiPb6Y2Cer8pvt4nmU25D0Sd6tXISdcsLXBZSZTWoj4GvLomOQoiqN7Xqqb5Amy8qKrWbFo/FVvIhyb39pErKi/fpVmCqoJrC6TwsJxXHcMfuEiC6jPqBgpAycX0EKtPm+LiBD6WGbqUpUvMLm6BPT8OeoqYo5yR17xKQqlXIlbxZkoyt3XGbOdcN0n7wgDDAaR3LhEpLA1tS0QzYHH3lDBCxG6z6DqGAt5PYa5O8cPKiqff/lkwyHQ/nnOt29ce4ndwvTCuqvg6HUF9gQsOcJ17tNrKBrzsKloX+nlhEsCX4BciuxmKlyNaDWgLPTRRHuefpmUUqSNhTrcJEUS4Qxvnn3NbEXcnqJNxEDR5yZIJgWp2rZKfY2JsmuFqNRYFSDkw0nf5f/1hf5XCpplz0juXrB7SMP1goF88LTq+JodkdyMqIhOHZ/YP8VdzvdRFfbYnEGoJHDYCsYORAD2pCzIzKZ2cI31iioFBoZi3BGimvpXHahSoEZ5I8=--Gb8wiTVoHYKfNwbY--t+5fkAnz5/hCL/6XeqqdEQ== \ No newline at end of file diff --git a/config/initializers/queenbee.rb b/config/initializers/queenbee.rb new file mode 100644 index 000000000..eda1c0048 --- /dev/null +++ b/config/initializers/queenbee.rb @@ -0,0 +1,6 @@ +Queenbee.host_app = Fizzy + +Rails.application.config.to_prepare do + Queenbee::Subscription.short_names = Subscription::SHORT_NAMES + Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token) +end diff --git a/config/initializers/tenanting.rb b/config/initializers/tenanting.rb index ce4193330..85ef97ba7 100644 --- a/config/initializers/tenanting.rb +++ b/config/initializers/tenanting.rb @@ -8,7 +8,7 @@ Rails.application.config.after_initialize do nil elsif subdomain = request.subdomain(tld_length).presence subdomain - elsif optional = request.path =~ %r{^/signup\b} + elsif request.path =~ %r{^/(queenbee|signup)\b} nil else "return-404" # this is a quick fix for now diff --git a/config/routes.rb b/config/routes.rb index ae77af74a..99ac09b83 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -158,6 +158,8 @@ Rails.application.routes.draw do root "events#index" + Queenbee.routes(self) + namespace :admin do mount MissionControl::Jobs::Engine, at: "/jobs" end diff --git a/test/models/account/signal_account_test.rb b/test/models/account/signal_account_test.rb index 62f5e4b87..cee8ba0bf 100644 --- a/test/models/account/signal_account_test.rb +++ b/test/models/account/signal_account_test.rb @@ -1,6 +1,9 @@ require "test_helper" class Account::SignalAccountTest < ActiveSupport::TestCase + # # TODO(MIKE): Queenbee client API compliance tests + # include Queenbee::Testing::Client + setup do @account = accounts("37s") end From a7f5ba55cdf2f1447f8bb316d7688bb13650ac83 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 19 Jun 2025 16:39:21 -0400 Subject: [PATCH 16/18] Script to create QB and SignalID accounts, and SignalID users --- script/bootstrap-signal-id.rb | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100755 script/bootstrap-signal-id.rb diff --git a/script/bootstrap-signal-id.rb b/script/bootstrap-signal-id.rb new file mode 100755 index 000000000..7618e8988 --- /dev/null +++ b/script/bootstrap-signal-id.rb @@ -0,0 +1,117 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" + +ActiveRecord::Base.logger = Logger.new(File::NULL) + +class BootstrapSignalId + def initialize(dry_run: false) + @dry_run = dry_run + end + + def run + SignalId::Database.on_master do + ApplicationRecord.with_each_tenant do |tenant| + puts "\n# tenant: #{tenant}" + + next unless check_account_preconditions + create_signal_id_account if Account.first.queenbee_id.nil? + + create_signal_id_users + end + end + end + + def check_account_preconditions + unless Account.count == 1 + puts "There are #{Account.count} accounts, but exactly one is expected." + false + else + true + end + end + + def create_signal_id_account + owner = SignalId::Identity.find_by_email_address!("kevin@37signals.com") + print_identity("New owner is:", owner) + + unless @dry_run + qbattr = queenbee_account_attributes(owner) + queenbee_account = Queenbee::Remote::Account.create!(qbattr) + + signal_id_account = SignalId::Account.find_by!(queenbee_id: queenbee_account.id) + signal_id_account.update_column :subdomain, ApplicationRecord.current_tenant + + account = Account.first + account.queenbee_id = queenbee_account.id + account.name = ApplicationRecord.current_tenant + account.save! + end + end + + def create_signal_id_users + signal_account = Account.first.signal_account + + User.find_each do |user| + if !user.system? && user.signal_user_id.nil? + signal_identities = SignalId::Identity.where(email_address: user.email_address) + if signal_identities.length > 1 + puts "Multiple identities found for #{user.email_address}:" + signal_identities.each { |identity| print_identity(" - ", identity) } + signal_identity = signal_identities.first + elsif signal_identities.length == 1 + signal_identity = signal_identities.first + print_identity("Identity for #{user.email_address}:", signal_identity) + else + puts "No identity found for #{user.name} (#{user.email_address})" + signal_identity = nil + end + + if signal_identity + unless @dry_run + signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account) + + user.signal_user_id = signal_user.id + user.save! + end + end + end + end + end + + def print_identity(message = "Identity:", identity) + pad = " " * message.length + puts "#{message} #{identity.name} (#{identity.email_address})" + puts "#{pad } ID: #{identity.id}" + puts "#{pad } Username: #{identity.username}" + end + + def queenbee_account_attributes(signal_identity) + { + skip_remote: true, # Fizzy creates its own local account + product_name: "fizzy", + name: account_name, + owner_identity_id: signal_identity.id, + trial: false, + subscription: subscription_attributes, + remote_request: request_attributes + } + end + + def subscription_attributes + subscription = FreeV1Subscription + { name: subscription.to_param, price: subscription.price } + end + + def request_attributes + { user_agent: "script/bootstrap-signal-id.rb" } + end + + def account_name + name = ApplicationRecord.current_tenant + name += " (Beta)" if Rails.env.beta? + name + end +end + +BootstrapSignalId.new(dry_run: false).run From 8b68eab6a8632ea9ef8cb6aee94ae785f17ced02 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Fri, 20 Jun 2025 12:50:19 -0400 Subject: [PATCH 17/18] Add the shared signal_id secret so that we can authenticate properly with launchpad --- config/credentials/beta.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/staging.yml.enc | 2 +- config/initializers/signal_id.rb | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/credentials/beta.yml.enc b/config/credentials/beta.yml.enc index 9cc8ba327..8eb55f4cd 100644 --- a/config/credentials/beta.yml.enc +++ b/config/credentials/beta.yml.enc @@ -1 +1 @@ -L6piL2TTSQHF7FMlMJRMK6JEUXI9AY6/DPMzr8wrwABiQ18LbcTNnLcDGjHvfC0PZCIQ7H355xnVQdDqDyyJhkFcrW91pW2yS17TI2ULQxgMAi28QR5K+rx2rBV/d665aWIpv7xgi6PgZuiZDAiu4p3ikheTBIo9/iEUa60qtOxsh66nI3hJGHnUhfOd0jhIxL2+ncDnLcWg3jaHFTj7zwmoTrD/npxWAkdmwdA8K6czjZv2UiV004ECCaOAS8rG2eF2BStIYoknTTOrmKhnHhSJbsYHCWCxgrD0pKIf8/5fjbyj/NKP578Qb8XTmMWi01sFM0APT1Nl2xX+4VzbeHNPHhy7BvtTfzDP5nr/vu0d7/1IpR9gYegzVIye5AWzNOYavG73Bz+lyCehcZ/EXuT1smNt/zPtuZMsxrfQ9i9Tv2BqkDTQ5vnz+BIGL7uNFdC4Mu7n4f1FjYdOhHVYqUv5MMITlkskRrx1uOWAGL6WwYdhXfzakFg02+OjEucNjAQIYjy3rglVBR/48zDVnpHfOKce2+3WfERSWbHHe+VnusQRFn53/KyeLGCHfNPUeZZ6RWh7OIm6T+aJFG7fJAJINt1Yoykm9zxi29Dq/+QMgW39dfIkf2ddTSFHzT4w6TKsVSfzvyfnB6L2Che15/Z8RMb/3iUALW/n5Vs11Iy1UrfTIdZ31a07yRfPr7NU4eGpE0Ee7UqMt/ALVqoj4tm9rTnzJm54im6czX27brLm3iHDi01973q/VpLlH8hDP56rBpIY0tEI0vPE47VX01wq/w4/FvHIIMuLy/qphes=--M7iv5/9byEuFFvYy--rJ+PG5eQLtQ2+KbGBi7gvA== \ No newline at end of file +yD+Wb/DYMXUXf16brcYmNb7OZngx7PuA6gLP8w6ZV7pA/GFweTIXQw67YoXebL6A7gWxhXX4tvpnrz9xPC1p5+5a8q00lOhcybc/480bSfenIXExDpg6mSDbQLRomrpwZsr+IqBWRC8WoXTtpVYRybEPRP88ZXdyE0MjnfOlGcr1fUcF3r9T8biXfNhS9v9UTxck1Q2yzGoNFRvBm3KgBN2QlJocG5AXaDYUSMfLoLmtkicO0KVOD7eYBxe0kDevUK7ZQGxbtATGIsjAJEifhuaPTj/Q2DH134TU4rlPr6n6vm9YEgC/CGVoqPIDgvovVd5IiqZlxGuVDDw3ct0cPH4/AnDLOVHiubi5/pZRCQyeOF+G1OmLVFUVeuZ6alPVrJSxHFxGVM/HT+pLWiReR/IJYacLzb3b2OOtBp/AKxbt25YxFFHuFHABfVEqPRo0qVwbQaxKZEyDyEW0t0ktTkqHywqkbmsx3HPU3skV42rTHf9c1udJhzTXZJPKIWKP96ahgX6eaOhy09rzwD99RnKLJuAuMl80Cw1L+ftLG5hM4tHYmv+JbpWDSInvP1jtA5iWtDRSiqhTxvXw4ItHximvsERI5k6hSPJzrzqnhLUxNVi10KQHh5KGq7EiC2J3HJwioY+fWVivzEJdw2WuVFsovcFyolrT3tyqcMVF5V3dQw4/gOGQ7gXsz0a9sb5wQgXJ9pyZBAmD4Bg7r4wD0f0JiL/5EdKOkAOTV2JDt0OgoSWzU+yuvByuWxPn2quoX0ZSG0huHvl2NK33RoR9aXLcSjtmi5XJGXpjATfKMoA2d8JBIDiJcOPAPwWkpI7AOnmvlV2SmrqPlLTkO0MfdJ528k8CFi2puSRQN+CEPhyhZclDO/J2IP94qmE35xAmWM7yjY6E+EZY9uhlF39uBhAIXylZwTyF31PSCqwpQJthH62pDFWpCgjy1iax482cCVt5n2HLQpmSNaPAvsI7hZAGFJsrDncwfJiK68YXrgmTeg8=--Gd2i+KrusH11oais--SraJ0tcCfEgIdRSJYoCoNw== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index 00bed20cf..e97e68755 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -R0KrQNC0YJq8Wiw/KEnP/UrW4mnbSOzaf9XupVUSr5N0E6sHCG9qTzM1eQUWoM2kLAdXjRzjHctg9o8qTOpMrs4PSEOn7LA4EELKs6TJ+pb2P6hM0pjAFyllUrGarR+d/AKkdVejYALIyPO0Hd7WeT+Zk2cS3u8YGjZwKcYJo7csBbHQL/Lx8Vn5yj2AR53khun1QeLL3Y7Yly9/1RK791of0p4F0TzjSZMq5WlxDP1seMiOpXRo/GLg+ZgkHfqWo4zqNnlovySHlVFM84MGYVqF/uf/4DnhfeD90n0MKNCUN2fKfZntEsoZ9FrdzAMzAcDPkocxBWMWvvC235hRlATEDWGmAsxR8Hv5T/KKdbChK3TGnry4k32IQ6uLxsFIrHnX+jzpPre+Zi7ObxAyiUU2f5UatfSBasocIfJwceh/qiLCifzPSeRg3aXfrZLZ+DrPgSHnzw4wO6sxt/S4KlQfQLfPtI5gmO/+T4tHYuVV4EZZLePiigOfFbhuLgbFS1+4Hxi+iOrMyxefcfMieGz5i/e+hTqvcZyKtI04zO0v6MsADKNayCAnVceShh6HNJ5sNUgy4eOSnYA0u9MkQstYj0fVZZVlaU7FXFZ1UgQJjnQYifwxKLk1qTSiQA618yuoRyVSMBWkXDpuAXeJKi6/BVTZ7NgFpratCL+j89c5nzOOR+F8U8F/o6n7BTc75nFgVsYvMb5NWLlSNmCqhMsDnn27FAMzpZ3yzg7SHFirMrgLXWgMGq548AhiFfoavMFLfvxq4Ft6+/ay5dSdCrRV73DBJYED4RPCv+5nIYraqEXd2AGUnomw+bjTlR5ayEIHX5c+kvbwCZjtsaVczeAYzSmIdthEFgJzO89duazlT+w7tHywQb4c5/JUE9RRos3f3ghJihTGXKdlkwFaIPt745dgkZesCSZVGV1osqF+VAq9Wd8heGzjrOwlf0dyMHVL1XQMhGDglh/AAtzOQnJrpZEn1NZweBo9Z+Dt7juLrkpjWjmuH/psBpeUQOUEiEFi1146c9csV/fcVVZHLT6Btj5vuTvKZOjrW7sEAGZW2c138j6t9tYbAJ8CxjniSw==--IShq57WkyKWLCJ99--ybcm6oObxU+anqaQD5lOeg== \ No newline at end of file +cPaSSbJ2XWIjNXDRi25iIH41V4zR1fKVTe3OV1XwZKsDE3950DL9Sk0RI/1bxYY+agT3x88vrsqm8xyqLwqcMeMg7J4el52WI4hORe5XLGJTcvs8CTgguut1s7c5Jb5ihOSPX9ZYEKnONxLU2BFe951WZgO+7Us2IBFbCQiqo8PXEBEONePuE9Aw2z1fUFW7NvxhptN9ws7DptP2O/wpHP8+KJmVuAJl+/qJHJoBh770dwQU3yCFz9mERowkARK4Z16Q+HelkrkvW9UziV53tMIExDRvfMnEKJz4wrU/AqVSiQIMQA6eVfTMQLxbVhBhLAlcV0Qic7yEbXLcAXwLX6Kga6aMOqyJvxh+JUnMpQavtHKikVncN8EI1lGxd/+mddS+khHZmCqkRvWl+LiaOe2WOkNENYXlGYANeRSTDy2BqMeT1ymAj3YxfWKYdR34Ukmm2iZKQ1d6fF+Dg/IT8/W/rIZPajQjMTO05WqeoE8kZ4lhgI7AxZHg9apHGsfkq6iBaf4kZUBFiLgOv0CR4Xz3zWVUHEyPx5afkkxs1u9NCspOaYWT4gA1tekFmaH7cMyhaQ/SEjOEs5AWyh47PeMBvwUumJKIEeil6p6K9109nSFG+BPszfhshY8G3aonuIbiWoJQAiGGnbvw4oZeAPjl8XNBYTWRspl2T9ep3krPg9L4eTrAyXdT0dG7GYXSa5NkrV86HugU5rDfx87N4QJVu6qr7Myzw6kEWlLRXHR6HPg+H7qZenNwuOfgnD3YoFOEWQfLiz9U3MqDRJIq5j+i2OfL4pv0VVpc51z42mWM1hvazGNiiTZvK0DohBja6D9h5Gl6ZP/eOGB8O6ZxIwn6mp23BIvxA5jBRGslCj/Op7msnkSAP0vtuI0hnCtfsOx0RfAPvj1/ETjVkRFhM7heWTgfqGQbjSVGvd/Dj6j3vbqGz3RASVL48juoWx/pezUMnWGZqRTqx5tD6FrZQy1yf/2Uoc8yMJQU14raqiShEqNveJswMv1D9kBnT3551DtpAmrMT+iuCX5aZHwVnY/sI8J8HdMAqFfpipMbCbccI/PgTc+U8uOqc+BBKYy8u04wHLnGu5wREewU5xRIkxcwGm4M4LkTLiVEJFUQuFEhylmxkCIDYG+WSBZ5xy/0dRUeZs26Uuvvs2//bXqn75Ia86Fpv54AzfdDqC9s0B1r5/ZqXtFjjL3bQyhv+SP5TMurAZcI0rLYay+oFcLZAv31zoo0dFcufiCKYTjbYEIlO+HW2aeVeOhUnCalqqslt4ql5A==--tVMd1KheRgH8F0gZ--x8Wzmk8JsMuHPG3JMOODLA== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 1dd42d7b4..ef3fa3ffb 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -ver09XH5FQD8dviqbC6DwvttqfTaL0UGRVtoS+qebJx22xjuaMSPXva21pCBj1vayxfEYytSz10/IQ/gYc6np/Th6lCLyljV6wnNigjgSRLYU9se9yU0yf3j6UmlDUphkxt6VGhXd/lS162rJnAlVPqu1+wBboqYuRcgeeGnnxbigKV+DnRpoQFeFy6rohMQNr0HFxfUbAMkkGWstAbKjWpOeeQrnAaj6jU7Ri4NnkF/I6KgeU1s0Wq95Kqe4ghytYHHBenTdIKOjZq7lqvnnt3TmOO5lJokA10r8ukLKrJ06BB56Y9HtrAYjrBGL4ky2bGYnvVS3EjSy7Vep5g5UO/ND5y353HCOwPuh8jxMudKhXfZoSRxl2QB4BDwWSTnALeX0t+KxHEbaz6u4q86miU2UseEgflA7Tx91WzYvA5aSJI81Y+TsFXTvBkU8BEqKiyA+BvcJA8kkAJYsbn9X7Vr36RBOPk9vhgEiDNp8ITStk/qEs2w446Dhnfjthce/UtaqDGHrm5mu3vR0QZa2Fo3rbBvhwx4uHJhHh163wJ9xdlIQ20PvuZFGf1sE2NdHD+d/dG3godJis2qyvz0wFIok5gW39hH3gTokroOaxiF0HE0rPiN7oVkBpSukeEHSXHE/X2+E/3NsjB5vDUtzcQASTrMKs1pldSsy9Fz0qwsKmq3W04qhs8DW94PzqhYONJiPb6Y2Cer8pvt4nmU25D0Sd6tXISdcsLXBZSZTWoj4GvLomOQoiqN7Xqqb5Amy8qKrWbFo/FVvIhyb39pErKi/fpVmCqoJrC6TwsJxXHcMfuEiC6jPqBgpAycX0EKtPm+LiBD6WGbqUpUvMLm6BPT8OeoqYo5yR17xKQqlXIlbxZkoyt3XGbOdcN0n7wgDDAaR3LhEpLA1tS0QzYHH3lDBCxG6z6DqGAt5PYa5O8cPKiqff/lkwyHQ/nnOt29ce4ndwvTCuqvg6HUF9gQsOcJ17tNrKBrzsKloX+nlhEsCX4BciuxmKlyNaDWgLPTRRHuefpmUUqSNhTrcJEUS4Qxvnn3NbEXcnqJNxEDR5yZIJgWp2rZKfY2JsmuFqNRYFSDkw0nf5f/1hf5XCpplz0juXrB7SMP1goF88LTq+JodkdyMqIhOHZ/YP8VdzvdRFfbYnEGoJHDYCsYORAD2pCzIzKZ2cI31iioFBoZi3BGimvpXHahSoEZ5I8=--Gb8wiTVoHYKfNwbY--t+5fkAnz5/hCL/6XeqqdEQ== \ No newline at end of file +UV9AzQwwjGoWFh78GyKkNewL4lmsL80XT+6ca0JIxZ632OMKbYzHbPtJAD6RK1gaEB4GRkiBE2mLEdk8nYI47p8K9NVqZuI/ykgl23Sr2JzBpnHfjtUYlPQC7nYyeAR3gYtASD5+jSXvP7z/9BquIVdTvih3hWPPujDK5OROXagDtlGTZ4SgKZwihCTZczTwZRLxx5UguwbtuowrtcuPTg+r/H+zdJyRUU9kF7vYqV5VrP0vj0mXZUf5hWNBpKvhDtqgKUJNU87ds7xTLwMuQT0MCASoJXS0JSpIKn653baXczQue6JyuFcOlqDPDmBWs2GLh6pq3XnNolmWoyNehD9WClmJ5cu0l1dPmYZ0WnOfPgJCPtuJI4onIUj2nLC5m0tHpQPNs9WjazQeCeiw7TJBESK3b3lB4/Q7STshfJd74IIJ0crzfm0a6MwxvT184etKpNRQkgJpwK6WRNPzzzHdlTmbkEher4ilcs3QrmyLPYaLcuG+0YyDcjgJXRvLumkZilONbuBHluMc64u4GAU8QyW1dp31DrCw/HTOTtPEsRJSKdTIiFFCDpYt0Uq16Qbp9Hsg1ruPqFPELXDqRSl+N9ixDdXjF8WyQc6Gx4i8Fsur0WBLGBEtCEkCCQvwsH+TVH47S68CSiUui0c2zX1sM2SEcOOL7Cn0TUDWg6d8MgHQ5977aGLGFUkeuldv8KfQSra5uHKESSxOwn/eJJQMERSiVHSEj1EK3v4R+shXnviPsFTr3uERjdn2s1CcSk3qJ7J5WFpmgTL3/qKRWDroI96QO24CSr8JWJ3MzrwkLLpiEf6qLyfswiRjJ79sxk1pTdx7zedb8MuG/596J/xFI7GdbOI6qr2NvVUHH6r6PqXltMBoT/JU4pO7QOXfuok8zYGOj2gMzktCOdvEoydh+8QrsKN6W3mpg8oweQoq/2tgRm/YTeJMz+MQMR+J+aid2lSu8/wiHtsv8WdxJ7qxI7G3DslzbTEz0e3FAP+CEFOVgwN+nitWZpSyst6WiupzU5Ou9NxYiWbmhy5nhDJFC+MiLs8bS2kWfq3z1/Wsuyp8hBn/T0+MYtQ/cRnAxTE0sUZMB5xpffuGPlx3lgrmxV7wlPAzGooCGkGVUQQ29ljYMoGWqW34EAniIYHLUnBfdS+bNbui5rxVEAFyD4r57eqEaYYOKCFwkhNfYhHUeV+noDQ8uvCzppvRHGKJrkUCS0/A6z4KP8iW8wsETMBX/ShvFf6m44KNGR4IPzfn+r7uO9Pbukit3RJ8RsHZcC3RmH7/gtO3vW8G1BBrT5nI6BdRp9a9LdTx2qn32cKQD7Wc7S1KyFYn5reWSewlxNHHmgy4R/pEXTtdZJ5tuzweeYK/jr78CTrgEEN/btNawNh6SdTwRls5lu33LAQ=--3mgI5AzJFOeaM6Y1--KgIowEbtW/iHXiDrWGk2ug== \ No newline at end of file diff --git a/config/initializers/signal_id.rb b/config/initializers/signal_id.rb index dc2c41417..c8804fda5 100644 --- a/config/initializers/signal_id.rb +++ b/config/initializers/signal_id.rb @@ -1,5 +1,7 @@ require "signal_id" +ENV["SIGNAL_ID_SECRET"] = Rails.application.credentials.signal_id_secret + Rails.application.config.to_prepare do SignalId.product = "fizzy" From 2c18173f08e68b4ba1af2f41d8f7f42114c2ed83 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Fri, 20 Jun 2025 13:42:48 -0400 Subject: [PATCH 18/18] Trampoline the Launchpad auth redirect from a GET into a POST to always route it to the writer. --- .../sessions/launchpad_controller.rb | 17 ++++++++++- app/views/sessions/launchpad/show.html.erb | 1 + config/routes.rb | 2 +- .../sessions/launchpad_controller_test.rb | 30 +++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 app/views/sessions/launchpad/show.html.erb create mode 100644 test/controllers/sessions/launchpad_controller_test.rb diff --git a/app/controllers/sessions/launchpad_controller.rb b/app/controllers/sessions/launchpad_controller.rb index 9409f2c85..0e2ad16a7 100644 --- a/app/controllers/sessions/launchpad_controller.rb +++ b/app/controllers/sessions/launchpad_controller.rb @@ -1,12 +1,27 @@ class Sessions::LaunchpadController < ApplicationController require_unauthenticated_access + before_action :store_sig, only: :show + before_action :restore_and_clear_sig, only: :update + def show - if user = Current.account.signal_account.authenticate(sig: params[:sig]).try(:peer) + end + + def update + if user = Current.account.signal_account.authenticate(sig: @sig).try(:peer) start_new_session_for user redirect_to after_authentication_url else render plain: "Authentication failed. This is probably a bug.", status: :unauthorized end end + + private + def store_sig + cookies[:_fizzy_launchpad_sig] = params.expect(:sig) + end + + def restore_and_clear_sig + @sig = cookies.delete :_fizzy_launchpad_sig + end end diff --git a/app/views/sessions/launchpad/show.html.erb b/app/views/sessions/launchpad/show.html.erb new file mode 100644 index 000000000..897e39922 --- /dev/null +++ b/app/views/sessions/launchpad/show.html.erb @@ -0,0 +1 @@ +<%= auto_submit_form_with method: :put %> diff --git a/config/routes.rb b/config/routes.rb index 99ac09b83..0e32a0b25 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,7 +85,7 @@ Rails.application.routes.draw do scope module: "sessions" do resources :transfers, only: %i[ show update ] end - get "launchpad", to: "sessions/launchpad#show" + resource :launchpad, only: %i[ show update ], controller: "sessions/launchpad" end namespace :signup do diff --git a/test/controllers/sessions/launchpad_controller_test.rb b/test/controllers/sessions/launchpad_controller_test.rb new file mode 100644 index 000000000..2ab50b2a0 --- /dev/null +++ b/test/controllers/sessions/launchpad_controller_test.rb @@ -0,0 +1,30 @@ +require "test_helper" + +class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest + test "show renders when not signed in" do + get session_launchpad_path(params: { sig: "test-sig" }) + + assert_response :success + + assert_equal cookies[:_fizzy_launchpad_sig], "test-sig" + end + + test "create establishes a session when the sig is valid" do + user = users(:david) + cookies[:_fizzy_launchpad_sig] = user.signal_user.perishable_signature + + put session_launchpad_path + + assert_redirected_to root_url + assert parsed_cookies.signed[:session_token] + end + + test "returns 401 when the sig is invalid" do + user = users(:david) + cookies[:sig] = "not-valid" + + put session_launchpad_path + + assert_response :unauthorized + end +end